Rust: `asm`(内联汇编)的跟踪问题

创建于 2015-11-09  ·  111评论  ·  资料来源: rust-lang/rust

此问题跟踪内联程序集的稳定性。 当前功能尚未通过 RFC 流程,可能需要在稳定之前完成。

A-inline-assembly B-unstable C-tracking-issue T-lang requires-nightly

最有用的评论

我想指出 LLVM 的内联 asm 语法与 clang/gcc 使用的语法不同。 差异包括:

  • LLVM 使用$0而不是%0
  • LLVM 不支持命名的 asm 操作数%[name]
  • LLVM 支持不同的寄存器约束类型:例如"{eax}"而不是 x86 上的"a"
  • LLVM 支持显式寄存器约束 ( "{r11}" )。 在 C 中,您必须改为使用 register asm 变量将值绑定到寄存器 ( register asm("r11") int x )。
  • LLVM "m""=m"约束基本上被打破了。 Clang 将这些转换为间接内存约束"*m""=*m"并将变量的地址传递给 LLVM 而不是变量本身。
  • 等等...

Clang 会将内联 asm 从 gcc 格式转换为 LLVM 格式,然后再将其传递给 LLVM。 它还对约束执行一些验证:例如,它确保"i"操作数是编译时常量,


鉴于此,我认为我们应该实现与 clang 相同的翻译和验证,并支持正确的 gcc 内联 asm 语法,而不是奇怪的 LLVM 语法。

所有111条评论

在稳定代码中确保内联汇编的向后兼容性会不会有什么困难?

@main-- 在https://github.com/rust-lang/rfcs/pull/1471#issuecomment -173982852 有一个很好的评论,我在这里复制以供后人使用:

随着所有开放式的错误不稳定性周边ASM()! (很多),我真的不认为这是准备稳定-尽管我很想有稳定的内联汇编中生锈。

我们还应该讨论今天的 asm!() 是否真的是最好的解决方案,或者类似于 RFC #129甚至D 的东西是否会更好。 这里要考虑的一个重点是 asm() 不支持与 gcc 相同的一组约束。 因此,我们可以:

  • 坚持 LLVM 行为并为此编写文档(因为我找不到任何文档)。 很好,因为它避免了 rustc 的复杂性。 不好,因为它会使来自 C/C++ 的程序员感到困惑,并且因为某些约束可能难以在 Rust 代码中模拟。
  • 模拟 gcc 并链接到他们的文档:很好,因为许多程序员已经知道这一点,并且有很多示例,只需稍加修改即可复制粘贴。 不好,因为它是编译器的重要扩展。
  • 做其他事情(就像 D 做的那样):很多工作可能会也可能不会有回报。 如果做得好,这在人体工程学方面可能远远优于 gcc 风格,同时可能与语言和编译器更好地集成,而不仅仅是一个不透明的 blob(这里有很多麻烦,因为我对编译器内部结构不够熟悉,无法评估这一点) .

最后,另一件要考虑的事情是#1201 ,它在目前的设计中(我认为)在很大程度上依赖于内联 asm - 或者内联 asm 做得对,就此而言。

我个人认为最好做 Microsoft 在 MSVC x64 中所做的事情:为每个 asm 指令定义一组(几乎)全面的内在函数,并仅通过这些内在函数执行“内联 asm”。 否则,很难优化内联 asm 周围的代码,这很讽刺,因为内联 asm 的许多用途都是为了性能优化。

基于内在的方法的一个优点是它不需要是一个全有或全无的东西。 您可以先定义最需要的内在函数,然后逐步构建设置。 例如,对于加密,有_addcarry_u64_addcarry_u32 。 请注意,执行内在函数的工作似乎已经非常彻底: https :

此外,即使最终决定支持内联汇编,添加内在函数也是一个好主意,因为它们使用起来更方便(根据我在 C 和 C++ 中使用它们的经验),所以从内在函数开始,看看我们能走多远似乎是一件错误风险为零的事情。

内在函数很好,但是asm!不仅仅可以用于插入指令。
例如,请参阅我在probe板条箱中生成 ELF 音符的方式。
https://github.com/cuviper/rust-libprobe/blob/master/src/platform/systemtap.rs

我希望这种黑客行为很少见,但我认为它仍然是一个有用的支持。

@briansmith

内联汇编对于想要进行自己的寄存器/堆栈分配(例如裸函数)的代码也很有用。

@briansmith是的,这些都是在可能的情况下使用内在函数的绝佳理由。 但是将内联汇编作为最终的出口舱口真是太好了。

@briansmith请注意, asm!()是 _kind of_ 内在函数的超集,因为您可以使用前者构建后者。 (反对这种推理的常见论点是,编译器理论上可以优化 _through_ 内在函数,例如将它们从循环中提升出来,对它们运行 CSE,等等。但是,这是一个非常强烈的对比,任何为 _optimization_ 目的编写 asm 的人都会做得更好无论如何都要比编译器。)另见https://github.com/rust-lang/rust/issues/29722#issuecomment -207628164 和https://github.com/rust-lang/rust/issues/29722# issuecomment -207823543 适用于内联 asm 有效但内在函数无效的情况。

另一方面,内在函数严重依赖于“足够智能的编译器”来实现_至少_通过手动汇编实现获得的性能。 我在这方面的知识已经过时,但除非取得重大进展,否则在许多(如果不是大多数)情况下,基于内在函数的实现仍然明显较差。 当然,它们使用起来更方便,但我想说的是,当程序员愿意深入到特定 CPU 指令的世界时,他们真的不太关心_那个_。

现在另一个有趣的考虑是内在函数可以与不支持它们的体系结构上的回退代码结合使用。 这为您提供了两全其美的优势:您的代码仍然是可移植的——它可以只使用一些硬件支持的硬件加速操作。 当然,这只有在非常常见的指令或应用程序具有一个明显的目标架构的情况下才能真正得到回报。 现在我提到这一点的原因是,虽然有人可能会争辩说这甚至可能是 _undesirable_ 与 _compiler-provided_ 内在函数(因为你可能关心你是否真的得到加速版本加上编译器复杂性从来没有好)我如果内在函数是由 _library_ 提供的(并且仅使用内联 asm 实现),则会说这是一个不同的故事。 事实上,这是我更喜欢的大局,尽管我可以看到自己使用内在函数而不是内联汇编。

(我认为 RFC #1199 中的内在函数与本次讨论有些正交,因为它们的存在主要是为了使 SIMD 工作。)

@briansmith

否则,很难优化内联 asm 周围的代码,这很讽刺,因为内联 asm 的许多用途都是为了性能优化。

我不确定你在这里的意思。 确实,编译器无法将 asm 分解为单独的操作以对其进行强度降低或窥视孔优化。 但至少在 GCC 模型中,编译器可以分配它使用的寄存器,在复制代码路径时复制它,如果它从未使用过就删除它,等等。 如果 asm 不是 volatile,GCC 有足够的信息来像对待任何其他不透明操作一样对待它,比如fsin 。 奇怪设计的整个动机是使内联汇编成为优化器可能会弄乱的东西。

但是我并没有经常使用它,尤其是最近没有使用它。 而且我对 LLVM 对该功能的再现没有经验。 所以我想知道发生了什么变化,或者我一直误解了什么。

我们在最近的工作周讨论了这个问题,因为@japaricno_std生态系统asm!宏作为更常用的功能之一。 不幸的是,我们没有看到稳定此功能的简单方法,但我想记下我们必须确保不会忘记这一切的注意事项。

  • 首先,我们目前没有很好的规范说明asm!宏中接受的语法。 现在它通常最终是“看看 LLVM”,上面写着“看看 clang”,上面写着“看看 gcc”,它没有很好的文档。 最后,这通常会在“阅读其他人的示例并对其进行调整”或“阅读 LLVM 的源代码”方面达到最低点。 为了稳定,最低限度是我们需要有语法和文档的规范。

  • 目前,据我们所知,LLVM 没有稳定性保证。 asm!宏是对 LLVM 现在所做的事情的直接绑定。 这是否意味着我们仍然可以根据需要自由升级 LLVM? LLVM 是否保证永远不会破坏这种语法? 缓解这种担忧的一种方法是让我们自己的层编译为LLVM 的语法。 这样我们就可以随时更改 LLVM,如果 LLVM 中内联汇编的实现发生变化,我们只需将翻译更新为 LLVM 的语法。 如果asm!变得稳定,我们基本上需要一些机制来保证 Rust 的稳定性。

  • 现在有很多与内联汇编相关的错误。 A-inline-assembly标签是一个很好的起点,它目前充斥着 ICE、LLVM 中的段错误等。总的来说,今天实施的这个功能似乎没有达到其他人期望的稳定质量保证Rust 中的功能。

  • 稳定内联汇编可能会使替代后端的实现变得非常困难。 例如,后端(如 miri 或起重机升降机)可能需要很长时间才能达到与 LLVM 后端相同的功能,具体取决于实现。 这可能意味着这里可以做的事情的一小部分,但在考虑稳定内联汇编时要记住这一点很重要。


尽管存在上面列出的问题,但我们希望确保至少有一些能力来推动这个问题! 为此,我们集思广益地讨论了一些如何推动内联组装趋于稳定的策略。 前进的主要方法是调查 clang 的作用。 大概 clang 和 C 具有有效稳定的内联汇编语法,并且我们可能可以反映 clang 所做的任何事情(尤其是 LLVM)。 更深入地了解 clang 如何实现内联汇编会很棒。 clang 有自己的翻译层吗? 它是否验证任何输入参数? (等等)

继续前进的另一种可能性是看看是否有一个组装器,我们可以从其他地方取下已经稳定的架子。 这里的一些想法是 nasm 或 plan9 汇编程序。 使用 LLVM 的汇编器在稳定性保证方面与 IR 中的内联汇编指令存在相同的问题。 (这是有可能的,但在使用之前我们需要一个稳定性保证)

我想指出 LLVM 的内联 asm 语法与 clang/gcc 使用的语法不同。 差异包括:

  • LLVM 使用$0而不是%0
  • LLVM 不支持命名的 asm 操作数%[name]
  • LLVM 支持不同的寄存器约束类型:例如"{eax}"而不是 x86 上的"a"
  • LLVM 支持显式寄存器约束 ( "{r11}" )。 在 C 中,您必须改为使用 register asm 变量将值绑定到寄存器 ( register asm("r11") int x )。
  • LLVM "m""=m"约束基本上被打破了。 Clang 将这些转换为间接内存约束"*m""=*m"并将变量的地址传递给 LLVM 而不是变量本身。
  • 等等...

Clang 会将内联 asm 从 gcc 格式转换为 LLVM 格式,然后再将其传递给 LLVM。 它还对约束执行一些验证:例如,它确保"i"操作数是编译时常量,


鉴于此,我认为我们应该实现与 clang 相同的翻译和验证,并支持正确的 gcc 内联 asm 语法,而不是奇怪的 LLVM 语法。

有一个关于 D、MSVC、gcc、LLVM 和 Rust 摘要的精彩视频,并附有在线幻灯片

作为一个希望能够在稳定的 Rust 中使用内联 ASM 并且比我想尝试从 Rust 访问一些 LLVM MC API 有更多经验的人,一些想法:

  • 内联 ASM 基本上是将一段代码复制粘贴到输出 .s 文件中,在一些字符串替换之后进行组装。 它还具有输入和输出寄存器的附件以及损坏的寄存器。 这个基本框架不太可能在 LLVM 中真正改变(尽管某些细节可能略有不同),我怀疑这是一个与框架无关的表示。

  • 构建从面向 Rust 的规范到面向 LLVM 的 IR 格式的转换并不难。 这可能是可取的——与 LLVM 的$和 GCC 的%符号不同,rust {}格式化语法不会干扰汇编语言。

  • LLVM 在实际识别哪些寄存器被破坏的实践中做得非常糟糕,特别是在不是由 LLVM 生成的指令中。 这意味着用户非常有必要手动指定哪些寄存器被破坏。

  • 尝试自己解析程序集可能是一场噩梦。 LLVM-C API 没有公开 MCAsmParser 逻辑,这些类在使用 bindgen 时很烦人(我已经做到了)。

  • 对于其他后端的可移植性,只要您将内联程序集主要保持在“使用一些寄存器分配和字符串替换复制粘贴此字符串”的级别,它就不应该过多地抑制后端。 删除整数常量和内存约束并只保留寄存器组约束应该不会造成任何问题。

我一直在玩一些游戏,看看可以用程序宏做什么。 我写了一个将 GCC 风格的内联汇编转换为 Rust 风格的https://github.com/parched/gcc-asm-rs。 我还开始研究一种使用 DSL 的方法,用户不必了解这些约束,而且它们都是自动处理的。

所以我得出的结论是,我认为 rust 应该只是稳定裸露的构建块,然后社区可以使用宏从树中迭代出来,以提出最佳解决方案。 基本上,只需使用“r”和“i”以及可能的“m”约束来稳定我们现在拥有的 llvm 样式,并且没有 clobbers。 其他约束和破坏可以稍后用他们自己的迷你 rfc 类型的东西来稳定。

就我个人而言,我开始觉得稳定这个功能是一项永远无法完成的艰巨任务,除非有人以某种方式聘请全职专家承包商来推动这一整年。 我想相信@parched的稳定asm!零碎建议的建议将使这变得容易处理。 我希望有人捡起它并运行它。 但是,如果不是,那么我们需要停止尝试寻求永远不会出现的令人满意的解决方案,而应寻求不令人满意的解决方案,该解决方案将:按原样稳定asm! 、疣、ICE、错误和所有,在宣传 jank 和不可移植性的文档中带有明亮大胆的警告,并打算在某一天弃用,如果一个令人满意的实现应该奇迹般地下降,上帝派来,在它的天堂主机上。 IOW,我们应该完全按照我们为macro_rules!所做的事情做(当然,就像macro_rules! ,我们可以有一段短暂的疯狂创可贴和防漏未来)。 我对替代后端的后果感到难过,但对于系统语言来说,将内联汇编降级为这种不确定性是可耻的,我们不能让假设的多个后端的可能性继续阻碍一个实际可用后端的存在。 我求求你,证明我错了!

系统语言将内联汇编降级为这种不确定性是可耻的

作为一个数据点,我现在碰巧正在开发一个依赖于gcc的板条箱,其唯一目的是用稳定的 Rust 发布一些 asm: https :


虽然它当然有其优势,但我对“稳定构建块并将其余部分留给 proc-宏”的方法持谨慎态度。 它本质上将设计、RFC 和实施过程外包给任何想做这项工作的人,可能没有人。 当然,稳定性/质量保证较弱是重点(权衡是有一些不完美的东西已经比什么都没有好得多),我理解这一点。

至少构建块应该是精心设计的——在我看来, "expr" : foo : bar : baz绝对不是。 我不记得在第一次尝试时就得到了正确的订单,我总是要查一下。 “用冒号分隔的魔术类别,您在其中指定带有魔术字符的常量字符串,这些魔术字符最终会对变量名称执行魔术操作,而您也只是以某种方式在其中混搭”是很糟糕的。

一个想法,...

今天,已经有一个名为 dynasm 的项目,它可以帮助您使用插件生成汇编代码,该插件用于使用一种 x64 代码对程序集进行预处理。

这个项目没有回答内联汇编的问题,但它肯定可以帮助,如果 rustc 提供一种将变量映射到寄存器的方法,并接受在代码中插入一组字节,这样的项目也可以用来填充 -这些字节集。

这样,从 rustc 的角度来看,唯一需要的标准化部分是能够在生成的代码中注入任何字节序列,并强制执行特定的寄存器分配。 这消除了对特定语言风格的所有选择。

即使没有动态,这也可以用作为 cpuid / rtdsc 指令制作宏的一种方式,这些宏只会被翻译成原始字节序列。

我想下一个问题可能是我们是否想向字节序列添加额外的属性/约束。

[编辑:我认为我在此评论中所说的任何话都不正确。]

如果我们想继续使用 LLVM 的集成汇编器(我认为这比生成外部汇编器更快),那么稳定意味着准确地稳定 LLVM 的内联汇编表达式和集成汇编器支持——并在发生任何变化时补偿这些变化。

如果我们愿意产生一个外部汇编器,那么我们可以使用我们想要的任何语法,但是我们放弃了集成汇编器的优点,并暴露在我们正在调用的任何外部汇编器中的变化中。

我认为即使 Clang 也不这样做,稳定在 LLVM 的格式上会很奇怪。 据推测,它确实在内部使用了 LLVM 的支持,但它提供了一个更像 GCC 的接口。

我 100% 同意说“Rust 完全支持 Clang 支持的内容”并称它为一天,特别是因为 AFAIK Clang 的立场是“Clang 完全支持 GCC 支持的内容”。 如果我们有一个真正的 Rust 规范,我们可以将语言软化为“内联汇编是实现定义的”。 优先级和事实上的标准化是强大的工具。 如果我们可以重新利用 Clang 自己的代码将 GCC 语法转换为 LLVM,那就更好了。 替代的后端问题不会消失,但理论上 GCC 的 Rust 前端不会太烦恼。 更少让我们设计,更少让我们无休止地骑自行车,更少让我们教,更少让我们维护。

如果我们根据 clang 支持的定义来稳定某些东西,那么我们应该称它为clang_asm!asm!名称应该保留给通过完整的 RFC 流程设计的东西,就像其他主要的 Rust 功能一样。 #自行车棚

我想在 Rust 内联汇编中看到一些东西:

  • 带有替换的模板模式很丑陋。 我总是在程序集文本和约束列表之间来回跳转。 简洁鼓励人们使用位置参数,这会使易读性变差。 符号名称通常意味着您将相同的名称重复三次:在模板中,命名操作数,以及绑定到操作数的表达式中。 Alex 的评论中提到的幻灯片显示,D 和 MSVC 允许您在代码中简单地引用变量,这看起来要好得多。

  • 约束既难以理解,又(主要)与汇编代码冗余。 如果 Rust 有一个带有足够详细的指令模型的集成汇编器,它就可以推断出对操作数的约束,从而消除错误和混乱的根源。 如果程序员需要指令的特定编码,则他们需要提供显式约束,但这通常不是必需的。

Norman Ramsey 和 Mary Fernández 写了一些关于新泽西机器代码工具包的论文,当时他们对以紧凑的方式描述汇编/机器语言对有很好的想法。 他们处理(Pentium Pro-era)iA-32 指令编码; 它完全不限于整洁的 RISC ISA。

我想再次重申最近一周工作

  • 今天,据我们所知,基本上没有关于此功能的文档。 这包括 LLVM 内部和所有内容。
  • 据我们所知,我们无法保证 LLVM 的稳定性。 就我们所知,LLVM 中内联汇编的实现随时可能发生变化。
  • 目前,这是 rustc 中一个非常有缺陷的功能。 它充满了(在编译时)段错误、ICE 和奇怪的 LLVM 错误。
  • 如果没有规范,几乎不可能想象为此提供替代后端。

对我来说,这是“如果我们现在稳定下来,我们保证将来会后悔”的定义,不仅是“后悔”,而且似乎很可能“导致实施任何新系统的严重问题”。

至少,我坚信子弹 (2) 不能妥协(也就是“稳定通道”中稳定的定义)。 放弃其他子弹会很伤心,因为它侵蚀了 Rust 编译器目前相当高的预期质量。

@jcranmer写道:

LLVM 在实际识别哪些寄存器被破坏的实践中做得非常糟糕,特别是在不是由 LLVM 生成的指令中。 这意味着用户非常有必要手动指定哪些寄存器被破坏。

我认为,在实践中,推断 clobber 列表会非常困难。 仅仅因为机器语言片段使用寄存器并不意味着它会破坏它; 也许它可以保存并恢复它。 保守的方法可能会阻止代码生成器使用可以使用的寄存器。

@alexcrichton写道:

据我们所知,我们无法保证 LLVM 的稳定性。 就我们所知,LLVM 中内联汇编的实现随时可能发生变化。

LLVM 文档保证“新版本可以忽略旧版本的功能,但不能错误编译它们。” (关于 IR 兼容性)。 这反而限制了他们可以改变内联汇编的程度,而且,正如我上面所说的,在 LLVM 级别实际上没有任何可行的替代品可以从当前情况下彻底改变语义(不像,例如,围绕着毒药和 undef 的持续问题)。 因此,说它的潜在不稳定性排除了将其用作 Rust asm!块的基础是有点不诚实的。 现在这并不是说它还有其他问题(糟糕的文档,虽然已经有所改善;限制性的糟糕;糟糕的诊断;以及不太常见的情况下的错误是我想到的)。

在阅读这篇文章时,我最大的担忧是我们让完美成为美好的敌人。 特别是,我担心寻找一些神奇的 DSL 中介将需要几年时间才能尝试将内联 asm 转化为可用的形式,因为人们发现集成 asm 解析器并试图让它们与 LLVM 一起工作会导致更多问题边缘情况。

LLVM 真的保证他们永远不会错误编译他们从未指定过行为的功能吗? 他们甚至如何确定更改是否是错误编译? 我可以在 IR 的其他部分看到它,但这似乎很值得期待。

我认为即使 Clang 也不这样做,稳定在 LLVM 的格式上会很奇怪。

Clang 没有这样做,因为它旨在能够编译为 GCC 编写的代码。 rustc 没有这个目标。 GCC 格式不是很符合人体工程学,所以最终我认为我们不希望这样,但是现在我不确定是否会更好。 有很多(每晚)使用当前 Rust 格式的代码,如果我们更改为 GCC 样式,这些代码就会中断,所以只有当我们能想出更好的东西时才值得更改。

至少构建块应该是精心设计的——在我看来, "expr" : foo : bar : baz绝对不是。

同意。 至少我更喜欢原始 LLVM 格式,其中约束和破坏都在一个列表中。 目前有一个冗余必须指定“=”前缀并将其放入输出列表中。 我还认为 LLVM 如何将它更像是一个函数调用,其中输出是表达式的结果,AFAIK 当前的asm!实现是 rust 中唯一具有“out”参数的部分。

LLVM 在实际识别哪些寄存器被破坏的实践中做得非常糟糕

AFAIK LLVM 不会尝试这样做,因为内联汇编的主要原因是包含一些 LLVM 不理解的代码。 它只进行寄存器分配和模板替换而不查看实际的程序集。 (显然它会在某个阶段解析实际程序集以生成机器代码,但我认为稍后会发生)

如果我们愿意产生一个外部汇编程序

我不确定是否可以使用集成的内联汇编器的替代方法,因为您必须如何让 LLVM 为其分配寄存器。 但是,对于全局组装,外部组装器将是可行的。

关于 LLVM 内联汇编器中的重大更改,我们与 Clang 处于同一条船上。 也就是说,如果它们进行了一些更改,我们只需要在它们发生时解决它们。

如果我们根据 clang 支持的定义来稳定某些东西,那么我们应该称它为 clang_asm!。 汇编! name 应该保留给通过完整的 RFC 流程设计的东西,就像其他主要的 Rust 功能一样。 #自行车棚

我全力以赴。 +1

有很多(每晚)使用当前 Rust 格式的代码,如果我们更改为 GCC 样式,这些代码就会中断,所以只有当我们能想出更好的东西时才值得更改。

@parched根据上面引用的@jimblandy的建议,任何使用asm!都将很高兴能够继续使用它。

今天,据我们所知,基本上没有关于此功能的文档。 这包括 LLVM 内部和所有内容。

如果 GCC 的汇编语法在 30 年后确实没有被指定或记录,那么似乎可以安全地假设要么产生记录的汇编子语言是一项非常困难的任务,以至于在我们有限的资源下它超出了 Rust 的能力,要么想要使用汇编的人根本不在乎。

据我们所知,我们无法保证 LLVM 的稳定性。 就我们所知,LLVM 中内联汇编的实现随时可能发生变化。

GCC/Clang 的内联汇编实现似乎不太可能改变,因为这会破坏自 90 年代以来编写的所有 C 代码。

没有规范,几乎不可能想象为此提供替代后端。

冒着冷酷无情的风险,如果 Rust 作为一种语言由于其令人尴尬的无法进入汇编而无法生存,那么替代后端的前景就毫无意义。 Nightly 是不够的,除非有人想默认 NightlyRust 的想法,这比 LLVM 变化的前景更能破坏 Rust 的稳定性保证。

放弃其他子弹会很伤心,因为它侵蚀了 Rust 编译器目前相当高的预期质量。

当我说每天我都感谢 Rust 开发人员的态度和他们坚持的巨大质量标准时,我并不是在撒谎(事实上,有时我希望你们都慢下来,这样你们就可以保持这种质量不会像布赖恩那样让自己筋疲力尽)。 然而,作为四年前luqmana方面没有看到任何进展,谁感到遗憾的是 Rust 中的加密仍然是不可能的,而且 SIMD在 Rust 中甚至没有解决方法,而跨平台接口正在慢慢确定,我感到沮丧。 如果我在这里显得强调,那是因为我认为这个问题对项目的生存至关重要。 此刻可能不是危机,但任何事情都需要时间来稳定下来,而且我们没有时间从头开始设计和实施世界一流的装配方言(事实证明了这一点)我们在过去四年中在这方面没有取得任何进展)。 Rust 需要在 2018 年的某个时候进行稳定的内联组装。我们需要现有技术来实现这一点。 macro_rules!情况承认有时越糟越好。 再一次,我恳求有人证明我是错的。

FWIW 和迟到派对我喜欢@florob的古龙水演讲。 没看过的小伙伴们可以看一下它的主要内容:

// Add 5 to variable:
let mut var = 0;
unsafe {
    asm!("add $5, {}", inout(reg) var);
}

// Get L1 cache size
let ebx: i32;
let ecx: i32;
unsafe {
    asm!(r"
        mov $$4, %eax;
        xor %ecx, %ecx;
        cpuid;
        mov %ebx, {};",
        out(reg) ebx, out(ecx) ecx, clobber(eax, ebx, edx)
    );
}
println!("L1 Cache: {}", ((ebx >> 22) + 1)
    * (((ebx >> 12) & 0x3ff) + 1)
    * ((ebx & 0xfff) + 1) * (ecx + 1));

下面的策略如何:将当前的asm重命名为llvm_asm (可能加上一些小改动)并声明它的行为是 LLVM 的实现细节,因此 Rust 稳定性保证没有完全扩展到它? 不同后端的问题应该或多或少地用target_feature类似的功能来解决,这取决于所使用的后端。 是的,这种方法会稍微模糊 Rust 的稳定性,但是像这样将程序集保持在不确定状态会以自己的方式破坏 Rust。

我已经向内部论坛发布了一个带有替代语法建议的 pre-RFC: https :

在我看来,最好的绝对是这里的好人的敌人。 我现在完全支持将gcc_asm!clang_asm!llvm_asm!宏(或其任何适当的子集)粘贴到具有兼容语法和语义的稳定中,同时制定出更好的解决方案. 我不认为永远支持这样的事情是一个巨大的维护负担:上面提出的更复杂的系统看起来很容易支持将旧式宏转换为新的语法糖精。

我有一个二进制程序http://[email protected]/BartMassey/popcount ,它需要 x86_64 popcntl指令的内联汇编。 这个内联程序集是每晚保留这段代码的唯一方法。 该代码源自一个已有 12 年历史的 C 程序。

现在,我的程序集的条件是

    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]

然后获取cpuid信息以查看popcnt是否存在。 在 Rust 中有一些类似于最近的 Google cpu_features 库https://opensource.googleblog.com/2018/02/cpu-features-library.html在 Rust 中的东西会很好,但是 c'est la vie。

因为这是一个演示程序,所以我想保留内联程序集。 对于真正的程序, count_ones()内在函数就足够了——除了让它使用popcntl需要将“-C target-cpu=native”传递给 Cargo,可能通过RUSTFLAGS (请参阅问题 #1137 和几个相关问题)因为将.cargo/config与我的源一起分发似乎不是一个好主意,这意味着现在我有一个名为 Cargo 的 Makefile。

简而言之,如果能在实际应用中使用 Intel 和其他公司的花哨的 popcount 指令就好了,但这似乎比实际需要的要难。 内在因素并不完全是答案。 当前asm!是一个不错的答案,如果它在稳定版中可用。 为内联汇编提供更好的语法和语义会很棒,但我真的不需要它。 能够直接在Cargo.toml指定target-cpu=native会很棒,但这并不能真正解决我的问题。

对不起,跑题了。 只是想我会分享我为什么关心这个。

@BartMassey我不明白,你为什么这么迫切需要编译成 popcnt? 我能看到的唯一原因是性能和 IMO,在这种情况下,您绝对应该只使用 count_ones()。 您正在寻找的不是内联 asm 而是 target_feature (rust-lang/rfcs#2045),因此您可以告诉编译器它允许发出 popcnt。

@BartMassey你甚至不需要为此使用内联汇编,只需使用coresimd cfg_feature_enabled!("popcnt")来查询运行二进制文件的 CPU 是否支持popcnt指令(它将如果可能,请在编译时解决此问题)。

coresimd还提供了保证使用popcnt指令的popcnt内在函数。

@gnzlbg

coresimd 还提供了保证使用 popcnt 指令的 popcnt 内在函数。

这有点题外话,但这种说法并不完全正确。 _popcnt64幕后使用leading_zeros ,因此如果popcnt功能不会被 crate 用户启用并且 crate 作者将忘记使用#![cfg(target_feature = "popcnt")]这个内在将得到编译成无效的程序集,并且没有防范措施

因此,如果 crate 用户不会启用 popcnt 功能

这是不正确的,因为内在函数使用#[target_feature(enable = "popcnt")]属性无条件地为内在函数启用popcnt功能,而与 crate 用户启用或禁用的内容无关。 此外, assert_instr(popcnt)属性确保在 Rust 支持的所有x86平台上,内在反汇编为popcnt

如果有人在 Rust 当前不支持的x86平台上使用 Rust,那么这取决于移植core以确保这些内在函数在该目标上生成popcnt


编辑: @newpavlov

因此,如果 crate 用户不启用 popcnt 功能并且 crate 作者将忘记使用 #![cfg(target_feature = "popcnt")] 这个内在函数将被编译为无效的程序集,并且没有针对它的保护措施。

至少在您在问题中提到的示例中,这样做会在程序中引入未定义的行为,在这种情况下,编译器可以做任何事情。 糟糕的代码生成器是一种可以得到的多种结果。

首先,对这次讨论的脱轨表示歉意。 只是想重申我的主要观点,即“我完全支持将 gcc_asm! 或 clang_asm! 或 llvm_asm! 宏(或其任何适当的子集)现在与语法和语义兼容的稳定版本中,同时制定出更好的解决方案。 ”

内联汇编的重点是这是一个 popcount 基准测试/演示。 我想要一个真正有保证的popcntl指令,尽可能作为基线和说明如何使用内联汇编。 我还想保证count_ones()在可能的情况下使用 popcount 指令,这样与 GCC 和 Clang 相比,Rustc 看起来不会很糟糕。

感谢您指出target_feature=popcnt 。 我会考虑如何在这里使用它。 我想我想对count_ones()进行基准测试,而不管用户为什么 CPU 编译,也不管它是否有 popcount 指令。 我只想确保目标 CPU 是否有 popcount count_ones()使用它。

stdsimd / coresimd crates 看起来不错,应该为这些基准测试启用。 谢谢! 对于这个应用程序,我希望尽可能少地使用标准语言功能之外的功能(我已经对lazy_static感到内疚)。 然而,这些设施看起来太好了,不容忽视,而且看起来它们正在成为“官方”的路上。

@nbp提出了一个想法,其中可能有一些实现从代码的某些表示到机器字节(可能是 proc-macro crate 或其他什么?),然后这些字节直接包含在代码中的特定位置。

将任意代码字节拼接到函数内的任意位置似乎是一个更容易解决的问题(尽管指定输入、输出及其约束以及clobbers 的能力仍然是必要的)。

抄送@eddyb

@nagisa这不仅仅是

@simias ,确实您必须指定变量如何与特定寄存器相关联,以及哪些寄存器被破坏,但所有这些都小于标准化任何汇编语言或任何 LLVM 汇编语言。

通过将程序集风格移动到驱动程序/过程宏,对字节序列进行标准化可能是最简单的方法。

使用逐字字节而不是正确的内联汇编的一个问题是,编译器无法进行寄存器 alpha 重命名,我不希望编写内联汇编的人也期望这样做。

但是,如果我想让编译器处理它,这将如何与寄存器分配一起工作? 例如,使用 GCC 的(残暴)语法:

asm ("leal (%1, %1, 4), %0"
     : "=r" (five_times_x)
     : "r" (x));

在这样的事情中,我让编译器分配寄存器,期望它会给我任何最方便和最有效的东西。 例如在 x86 64 上,如果five_time_x是返回值,那么编译器可能会分配eax并且如果x是一个函数参数,它可能已经在某个寄存器中可用。 当然,编译器只有在编译序列的后期才确切地知道它将如何分配寄存器(特别是如果它不像简单的函数参数和返回值那么简单)。

您提出的解决方案是否适用于这样的事情?

@nbp我不得不说我对这个提议有点困惑。
首先,标准化汇编语言从来都不是我们想要通过内联汇编实现的。 至少对我来说,前提始终是系统汇编程序使用的汇编语言会被接受。
问题不在于解析/组装程序集,我们可以轻松地将其传递给 LLVM。
问题在于填充模板化程序集(或为 LLVM 提供所需的信息),并指定输入、输出和 clobbers。
你的提议实际上并没有解决后面的问题。 然而,它得到了缓解,因为您不会/不能支持寄存器类( @simias询问),而只是具体的寄存器。
在将约束简化到该扩展的点上,实际上支持“真正的”内联汇编同样容易。 第一个参数是一个包含(非模板化)程序集的字符串,其他参数是约束。 这在某种程度上很容易映射到 LLVM 的内联汇编器表达式。
另一方面,据我所知(或者可以从 LLVM IR 参考手册中得知),LLVM 不支持插入原始字节。 因此,我们基本上会扩展 LLVM IR,并使用单独的 crate 重新实现 LLVM 中已经存在的功能(组装系统组装)。

@nbp

实际上,您必须指定变量如何与特定寄存器相关联,以及哪些寄存器被破坏,但所有这些都小于标准化任何汇编语言或任何 LLVM 汇编语言。

那怎么做呢? 我有一个带有硬编码寄存器的字节序列,基本上意味着输入/输出寄存器、clobbers 等都被硬编码在这个字节序列中。

现在我将这些字节注入到我的 Rust 二进制文件中。 我如何告诉 rustc 哪些寄存器是输入/输出,哪些寄存器被破坏等? 这是一个比稳定内联汇编更小的问题要解决的问题吗? 在我看来,这正是内联汇编所做的,只是可能有点难,因为现在需要在编写的汇编中两次指定输入/输出 clobbers,并且我们以任何方式将此信息传递给 rustc。 此外,rustc 不会很容易验证这一点,因为为此它需要能够将字节序列解析为程序集,然后进行检查。 我错过了什么?

@simias

asm ("leal (%1, %1, 4), %0"
     : "=r" (five_times_x)
     : "r" (x));

这是不可能的,因为原始字节不允许寄存器的 alpha 重命名,并且寄存器必须由前面的代码序列强制执行。

@Florob

至少对我来说,前提始终是系统汇编程序使用的汇编语言会被接受。

我的理解是,依赖系统汇编器不是我们想要依赖的东西,而是作为 asm 的一部分被接受的缺陷! 宏。 还靠asm! 作为 LLVM 语法对于额外后端的开发来说是痛苦的。

@gnzlbg

那怎么做呢? 我有一个带有硬编码寄存器的字节序列,基本上意味着输入/输出寄存器、clobbers 等都被硬编码在这个字节序列中。

这个想法是有一个输入、输出和被破坏的寄存器列表,其中输入将是与(可变)引用或副本相关联的寄存器名称的元组,被破坏的寄存器将是一个寄存器名称列表,以及输出将是一个输出寄存器列表,它将形成一个关联类型的命名寄存器元组。

fn swap(a: u32, b: u32) -> (u32, u32) {
  unsafe{
    asm_raw!{
       bytes: [0x91],
       inputs: [(std::asm::eax, a), (std::asm::ecx, b)],
       clobbered: [],
       outputs: (std::asm::eax, std::asm::ecx),
    }
  }
}

此代码序列可能是某个编译器过程宏的输出,可能如下所示:

fn swap(a: u32, b: u32) -> (u32, u32) {
  unsafe{
    asm_x64!{
       ; <-- (eax, a), (ecx, b)
       xchg eax, ecx
       ; --> (eax, ecx)
    }
  }
}

这些序列将无法直接嵌入任何符号或地址,它们必须作为寄存器进行计算和给出。 我相信我们以后可以弄清楚如何添加在字节序列中插入一些符号地址的能力。

这种方法的优点是只需要标准化寄存器和约束列表,这很容易被任何未来的后端支持。

@nbp

我的理解是,依赖系统汇编器不是我们想要依赖的东西,而是作为 asm 的一部分被接受的缺陷! 宏。 还靠asm! 作为 LLVM 语法对于额外后端的开发来说是痛苦的。

我不认为这是一个准确的评估? 除了 x86 汇编的两种不同语法外,汇编语法在很大程度上是标准的和可移植的。 系统汇编器的唯一问题可能是它缺少更新的指令,但这是一个不值得优化的利基情况。

实际的问题是胶入寄存器分配。 但是,就实际的程序集字符串本身而言,这仅意味着有人必须进行一些字符串替换,也许还需要进行一些解析——而且这种替换对于任何假定的后端都应该是微不足道的。

我同意 LLVM(或 gcc)的这些东西的语法是废话,但转向预编译字节意味着任何 asm crate 现在都需要安装一个完整的汇编程序和一个完整的寄存器分配器(或让程序员手动分配寄存器),或者尝试使用系统汇编器。 在这一点上,它似乎并没有真正增加多少价值。

@jcranmer

...但转向预编译字节意味着任何 asm crate 现在都需要安装完整的汇编程序和可能的完整寄存器分配器(或让程序员手动分配寄存器),或尝试使用系统汇编程序

https://github.com/CensoredUsername/dynasm-rs

这个 crate 使用由插件处理的宏来组装汇编代码并生成要在运行时连接的原始汇编代码的向量。

@nbp也许我的用例很奇特,但缺少寄存器重命名并让编译器为我分配寄存器会有点

如果程序集 blob 不能与周围的编译器发出的程序集很好地集成,我不妨在独立的 .s 程序集文件中的外部 C 样式方法中考虑 ASM 存根,因为函数调用具有相同类型的寄存器-分配约束。 这在今天已经有效,尽管我认为与独立的程序集文件相比,将它内置到 rustc 可能会简化构建系统。 我想我想说的是,与目前的情况相比,IMO 你的提议并没有让我们走得太远。

如果 ASM 代码调用将由链接器解析的外部符号怎么办? 您需要传递这些信息,因为直到编译过程后期才可能解决这些问题。 您必须将引用与字节数组一起传递,并让链接器稍后解析它们。

@jcranmer

除了 x86 汇编的两种不同语法外,汇编语法在很大程度上是标准的和可移植的。

我不确定我理解你的意思,显然 ASM 语法不能跨架构移植。 即使在同一个架构中,也经常会有变化和选项来改变语言的组装方式。

我可以以 MIPS 为例,有两个重要的配置标志可以调整汇编器的行为: atreorderat表示在汇编某些伪指令时是否允许汇编器隐式使用AT (汇编器临时)寄存器。 显式使用AT存储数据的代码必须与at组合在一起,否则会出错。

reorder定义了编码器是手动处理分支延迟槽还是他们信任汇编器来处理它们。 使用错误的reorder设置组装代码几乎肯定会生成伪造的机器代码。 当您编写 MIPS 程序集时,您必须始终了解当前模式,如果它包含任何分支指令。 例如,如果您不知道reorder是否已启用,就不可能知道此 MIPS 列表的含义:

    addui   $a0, 4
    jal     some_func
    addui   $a1, $s0, 3

32 位 ARM 程序集具有 Thumb/ARM 变体,了解您的目标指令集很重要(并且您可以在函数调用中动态更改)。 混合两组需要非常小心。 ARM 代码通常还使用与 PC 相关的隐式加载来加载大的立即数,如果您预汇编代码,则必须小心如何传递这些值,因为它们必须保持在附近但不是实际指令一个明确的位置。 我说的是伪指令,如:

   ldr   r2, =0x89abcdef

另一方面,MIPS 倾向于将立即数拆分为两个 16 位值并使用 lui/ori 或 lui/andi 组合。 它通常隐藏在li / la伪指令后面,但是如果您使用noreorder编写代码并且不想浪费延迟槽有时您必须处理手动生成有趣的代码:

.set noreorder

   /* Display a message using printf */
   lui $a0, %hi(hello)
   jal printf
   ori $a0, %lo(hello)

.data

hello:
.string "Hello, world!\n"

%hi%lo构造是一种告诉程序集分别生成对hello符号的高 16 位和低 16 位的引用的方法。

一些代码需要非常特殊的对齐约束(例如,当您处理缓存失效代码时很常见,您需要确保不要看到您所在的分支)。 正如我前面提到的,在编译过程中此时无法解决处理外部符号的问题。

我确信我可以为我不太熟悉的其他一些架构提出特殊性。 由于这些原因,我不确定我对宏/DSL 方法是否非常乐观。 我知道在代码中间有一个随机的不透明字符串文字并不是非常优雅,但我真的不明白将完整的 ASM 语法以一种或另一种方式集成到 rust 会给我们带来什么,除了在添加支持时额外的头痛一种新的架构。

编写汇编程序乍一看似乎微不足道,但如果您想支持所有架构的所有花里胡哨和怪癖,则可能会变得非常棘手。

另一方面,有一个很好的方法来指定绑定和 clobbers 将是非常有价值的(与 gcc 的...完美的语法相比)。

嗨,大家好,

抱歉打扰您,我只想放弃我的两分钱,因为我只是一个用户,而且确实是一个非常害羞/安静的人,哦,还有一个新人,我最近刚刚登陆 Rust,但我已经爱上它。

但是这个组装的事情太疯狂了,我的意思是,这是一个为期三年的对话,有很多想法和抱怨,但似乎没有什么是最低限度的共识。 三年而不是RFC,这似乎有点像死胡同。 我正在开发一个简陋的数学库(希望能在两三个板条箱中实现),对我来说(我怀疑对于任何其他对用 Rust 编写汇编感兴趣的人),最重要的是实际上能够做吧! 最低限度地保证第二天一切都不会改变(这就是不稳定的频道,特别是这次谈话,让我觉得)。

我知道这里的每个人都想要最好的解决方案,也许有一天有人提出了那个解决方案,但至于今天,我相信当前的宏很好(好吧,也许在某些方面有点限制,但希望没有什么不能以增量方式解决)。 编写汇编就像系统语言中最重要的事情,一个非常非常必要的功能,虽然我可以依靠 cpp_build 直到这个问题得到解决,但我很害怕如果花费更多的时间它会变得永远的依赖。 不知道为什么,称之为不合理的想法,但我发现不得不调用cpp来调用程序集有点难过,我想要一个纯锈的解决方案。

FWIW Rust 在这里并没有那么特别,MSVC 也没有 x86_64 的内联 asm。 他们确实有一个非常奇怪的实现,您可以将变量用作操作数,但仅适用于 x86。

@josevalaad你能多谈谈你使用内联汇编的目的吗?

我们通常只看到它在类似操作系统的情况下使用,由于其他原因,这些情况通常每晚都被卡住,即便如此,他们也几乎不使用asm! ,因此稳定asm!并不是设计和开发可以在 LLVM 之外正常生存并取悦所有人的东西的优先级足够高。

此外,大多数事情都可以使用公开的平台内在函数来完成。 x86x86_64已经稳定,其他平台正在开发中。 大多数人期望这些将完成 95-99% 的目标。 您可以将我自己的 crate jetscii作为使用一些内在函数的示例。

我们刚刚合并了一个 jemalloc PR,它使用内联汇编来解决 LLVM 中的代码生成错误 - https://github.com/jemalloc/jemalloc/pull/1303 。 有人在这个问题 (https://github.com/rust-lang/rust/issues/53232#issue-349262078) 中使用了内联汇编来解决在 jetscii 板条箱中发生的 Rust (LLVM) 代码生成错误。 两者都发生在过去两周内,在这两种情况下,用户都尝试使用内部函数,但编译器失败了。

当 C 编译器的代码生成不可接受时,最坏的情况是用户可以使用内联汇编并继续在 C 中工作。

当这种情况发生在稳定的 Rust 中时,现在我们必须告诉人们使用不同的编程语言或等待不确定的时间(通常以年为单位)。 那不好。

@eddyb好吧,我正在编写一个小型矩阵代数库。 在那个库中,我正在实现 BLAS,也许是一些 LAPACK(还没有)在 Rust 中的例程,因为我希望库是一个纯粹的 Rust 实现。 这还不是什么严重的事情,但无论如何,我希望用户能够选择一些 asm 速度和乐趣,特别是对于 GEMM 操作,这是必不可少的(最常用的,无论如何,如果你遵循 BLIS 人的方法这就是您所需要的),至少在 x86/x86_64 中是这样。 这就是完整的故事。 显然我也可以使用夜间频道,我只是想在功能稳定的实用方向上做一些推动。

@shepmaster有_大量_的内在函数不够用的用例。 在我最近的想法中,我想“为什么为什么 Rust 没有稳定的 asm?”,没有 XACQUIRE/XRELEASE 内在函数。

稳定的内联汇编至关重要,不,内在函数是不够的。

我最初的观点是试图帮助某人有能力编写更快的代码。 他们没有提到知道甚至可以使用内在函数,这就是我想要分享的全部内容。 其余的是背景信息。

我什至不主张特定的观点,所以请不要试图与我争论——我在这场比赛中没有任何利益。 我只是根据我的理解重复当前的观点。 我参与了一个需要内联汇编的项目,该项目在不久的将来极不可能具有内在函数,因此我也对一些稳定的内联汇编感兴趣,但每晚的汇编不会过度打扰我,也不会调用汇编程序。

是的,有些情况现在需要组装,有些情况将永远需要它,我最初也说过(为了清楚起见,我强调了一点):

大多数人的期望是 [intrinsics] 将完成95-99% 的目标

我的观点是,如果你想看到稳定的组装,某人(或一群人)将需要从 Rust 团队就开始的方向获得普遍共识,然后投入大量精力来实现它.

这还不是什么严重的事情,但无论如何,我希望用户能够选择一些 asm 速度和乐趣,特别是对于 GEMM 操作,这是必不可少的(最常用的,无论如何,如果你遵循 BLIS 人的方法这就是您所需要的),至少在 x86/x86_64 中是这样。

我仍然不明白您需要访问哪些指令,而没有内联汇编则无法访问。 或者它只是一个特定的算术指令序列?
如果是这样,您是否针对内联程序集对等效的 Rust 源进行了基准测试?

您需要访问哪些指令,而没有内联汇编则无法访问

好吧,当您在数学中谈论汇编时,您基本上是在谈论使用 SIMD 寄存器和指令(如 _mm256_mul_pd、_mm256_permute2f128_pd 等)以及进行向量化的操作。 问题是您可以采用不同的方法进行矢量化,通常需要反复试验才能获得针对您的目标处理器和您想到的用途的优化性能。 因此,通常在库级别,您首先必须查询注入 asm 代码的处理器以了解支持的指令集和寄存器,然后有条件地编译特定版本的数学 asm 内核。

如果是这样,您是否针对内联程序集对等效的 Rust 源进行了基准测试?

现在我手头没有特定的测试,而且我正在度假,所以我不想让自己参与太多,但是是的,如果你给我几个星期,我可以发布一个性能比较。 在任何情况下,编译器过去不可能像手动调整汇编一样快速生成代码。 至少在 C 中是不可能的,即使您在需要的地方使用手动循环展开等经典性能技术,所以我认为在 Rust 中应该不可能。

Taylor Cramer 建议我在这里发帖。 请原谅我,因为我没有通读所有评论来加快讨论的当前状态; 这只是对我们情况的支持和声明。

对于 Google 的一个裸机项目,我们希望看到在稳定内联和模块级汇编器方面的一些进展。 另一种方法是使用 FFI 调用以纯汇编语言编写并单独组装并链接到二进制文件中的函数。

我们可以在汇编程序中定义函数并通过 FFI 调用它们,在一个单独的步骤中将它们链接起来,但我知道没有一个严肃的裸机项目专门这样做,因为它在复杂性和性能方面都有缺点。 Redox 使用“asm!”。 Linux、BSD、macOS、Windows 等的常见嫌疑人都大量使用内联汇编器。 锆石和 seL4 做到了。 甚至在几年前的 Harvey fork 中,Plan 9 也对此做出了妥协。

对于性能关键的事情,函数调用开销可能占主导地位,具体取决于被调用函数的复杂性。 就复杂性而言,定义单独的汇编函数只是为了调用单个指令、读取或写入寄存器或以其他方式操作通常对用户空间程序员隐藏的机器状态意味着更繁琐的样板会出错。 无论如何,我们必须在使用 Cargo 时更有创意(或补充一个外部构建系统或 shell 脚本或其他东西)来做到这一点。 也许 build.rs 可以在这里提供帮助,但将其输入链接器似乎更具挑战性。

如果有某种方法可以将符号常量的值连接到汇编程序模板中,我也非常喜欢它。

我们很想看到一些关于稳定内联和模块级汇编程序的动向。

上一个 pre-RFC(https://internals.rust-lang.org/t/pre-rfc-inline-assembly/6443)在 6 个月前达成共识(至少在大多数基本问题上),所以下一步是提交一个基于此的 RFC。 如果您希望这更快地发生,我建议您联系@Florob

对于它的价值,我需要直接访问 FSGS 寄存器以获取指向 Windows 上 TEB 结构的指针,我还需要一个_bittest64 -like 内在函数将bt应用于任意内存位置,没有内联汇编或外部调用,我都找不到方法。

不过,这里提到的第三点与我有关,因为 LLVM 确实更喜欢 Just Crash,如果出现问题,则不提供任何错误消息。

@MSxDOS

我还需要一个类似 _bittest64 的内在函数来将 bt 应用到任意内存位置,我无法找到没有内联汇编或外部调用的方法。

将它添加到stdsimd应该不难,clang 使用内联汇编实现这些(https://github.com/llvm-mirror/clang/blob/c1c07cca8cae5f924cedaac7b202b0f3c167111d/test/CodeGen/bittest-intrin .c#L45) 但我们可以在 std 库中使用它,并将内在的暴露给安全的 Rust。

鼓励在 stdsimd 存储库中打开有关缺少的内在函数的问题。

@josevalaad

好吧,当您在数学中谈论汇编时,您基本上是在谈论使用 SIMD 寄存器和指令(如 _mm256_mul_pd、_mm256_permute2f128_pd 等)以及进行向量化的操作。

啊,我怀疑可能是这样。 好吧,如果您想尝试一下,您可以将程序集转换为std::arch内在调用,看看是否能从中获得相同的性能。

如果没有,请提交问题。 LLVM 不是魔术,但至少内在函数应该和 asm 一样好。

@dancrossnyc如果您不介意我问一下,在您的情况下,是否有任何需要内联汇编的用例/平台功能?

@MSxDOS也许我们应该公开内部函数来读取“段”寄存器?


也许我们应该进行一些数据收集,并详细了解人们真正想要asm!用途,看看其中有多少可以通过其他方式得到支持。

也许我们应该做一些数据收集并了解人们真正想要的 asm!

我想要asm!用于:

  • 解决编译器未提供的内在函数
  • 解决编译器错误/次优代码生成
  • 执行无法通过一系列单个内部函数调用执行的操作,例如,读取 EFLAGS-modify-write EFLAGS,其中允许 LLVM 在读取和写入之间修改 eflags,并且 LLVM 还假定用户不会修改这在背后(也就是说,安全地使用 EFLAGS 的唯一方法是将读取-修改-写入操作写为单个原子asm!块)。

看看有多少可以通过其他方式得到支持。

我没有看到任何其他方式来支持任何不涉及某种形式的内联汇编的用例,但我的想法是开放的。

复制自我在 RFC 前线程中的帖子,这是我在当前项目中使用的一些内联程序集 (ARM64):

// Common code for interruptible syscalls
macro_rules! asm_interruptible_syscall {
    () => {
        r#"
            # If a signal interrupts us between 0 and 1, the signal handler
            # will rewind the PC back to 0 so that the interrupt flag check is
            # atomic.
            0:
                ldrb ${0:w}, $2
                cbnz ${0:w}, 2f
            1:
               svc #0
            2:

            # Record the range of instructions which should be atomic.
            .section interrupt_restart_list, "aw"
            .quad 0b
            .quad 1b
            .previous
        "#
    };
}

// There are other versions of this function with different numbers of
// arguments, however they all share the same asm code above.
#[inline]
pub unsafe fn interruptible_syscall3(
    interrupt_flag: &AtomicBool,
    nr: usize,
    arg0: usize,
    arg1: usize,
    arg2: usize,
) -> Interruptible<usize> {
    let result;
    let interrupted: u64;
    asm!(
        asm_interruptible_syscall!()
        : "=&r" (interrupted)
          "={x0}" (result)
        : "*m" (interrupt_flag)
          "{x8}" (nr as u64)
          "{x0}" (arg0 as u64)
          "{x1}" (arg1 as u64)
          "{x2}" (arg2 as u64)
        : "x8", "memory"
        : "volatile"
    );
    if interrupted == 0 {
        Ok(result)
    } else {
        Err(Interrupted)
    }
}

@Amanieu请注意, @japaric正在努力实现 ARM 的内在函数。 值得检查该提案是否满足您的需求。

@shepmaster

@Amanieu请注意, @japaric正在努力实现 ARM 的内在函数。 值得检查该提案是否满足您的需求。

值得一提的是:

  • 这项工作不会取代内联汇编,它只是补充它。 这种方法在std::arch实现了供应商 API,这些 API 对于某些人来说已经不够用了。

  • 这种方法只有在像foo(); bar(); baz();这样的内部调用序列产生的代码与该指令序列无法区分时才可用 - 不一定是这种情况,如果不是,看起来正确的代码最多只能产生不正确的代码结果,并且在最坏的情况下具有未定义的行为(我们已经在x86x86_64中的std ,例如,https://github.com/rust- lang-nursery/stdsimd/blob/master/coresimd/x86/cpuid.rs#L108 - 其他架构也有这些问题)。

  • 某些内在函数具有立即模式参数,您无法通过函数调用传递这些参数,因此foo(3)将不起作用。 这个问题的每个解决方案目前都是一个奇怪的解决方法,在某些情况下,Rust 目前没有可能的解决方法,所以我们只是不提供其中一些内在函数。

因此,如果供应商 API 可以在 Rust 中实现,在std::arch上可用,并且可以组合起来解决问题,我同意它们比内联汇编更好。 但是时不时地,要么 API 不可用,甚至可能无法实现,和/或它们无法正确组合。 虽然我们可以在未来解决“可实现性问题”,但如果您想要做的不是由供应商 API 公开的,或者 API 无法组合,这种方法将无济于事。

LLVM 的内在函数(尤其是 SIMD)实现非常令人惊讶的是,它们根本不符合英特尔内在函数到指令的显式映射——它们受到广泛的编译器优化。 例如,我记得有一次我试图通过从其他常量计算一些常量而不是从内存加载它们来减少内存压力。 但是 LLVM 只是继续将整个事物不断折叠回我试图避免的确切内存负载。 在另一种情况下,我想研究用 8 位 shuffle 替换 16 位 shuffle 以降低 port5 压力。 然而,一直有用的 LLVM 优化器以其无穷无尽的智慧注意到我的 8 位 shuffle 实际上是 16 位 shuffle 并替换了它。

这两种优化肯定会产生更好的吞吐量(尤其是在面对超线程时),但不会产生我希望实现的延迟减少。 我最终在那个实验中一路下降到 nasm,但不得不将代码从内在函数重写为普通的 asm 只是不必要的摩擦。 当然,我希望优化器在使用一些高级矢量 API 时处理指令选择或常量折叠之类的事情。 但是当我明确决定使用哪些指令时,我真的不希望编译器乱七八糟。 唯一的选择是内联汇编。

因此,如果供应商 API 可以在 Rust 中实现,在std::arch上可用,并且可以组合起来解决问题,我同意它们比内联汇编更好

这就是我一开始所说

完成 95-99% 的目标

然后再次

是的,有些情况现在需要组装,有些情况将永远需要它,我最初也说过(为了清楚起见,我强调了一点):

大多数人的期望是[内在函数] 将完成 95-99% 的目标。

这与@eddyb同时说的相同。 我不清楚为什么很多人表现得好像我完全无视内联汇编的用处,同时试图指出当前情况的现实

我有

  1. 指着一张海报,他没有提到知道内在函数存在于今天的稳定内在函数。
  2. 将另一张海报指向提议的内在函数,以便他们可以为提议提供早期反馈。

让我非常清楚地说明这一点:是的,有时需要内联汇编并且很好。 我不是在争论那个。 我只是想用现在可用的工具帮助人们解决现实世界的问题。

我想说的是,我们应该对此采取更有条理的方法,进行适当的调查,并收集比此线程中的少数人更多的数据,然后用它来指出最常见的需求内联汇编(因为很明显内在函数不能完全取代它)。

怀疑每个架构都有一个棘手的模型子集,可以从内联asm!中得到一些使用,也许我们应该关注这些子集,然后尝试进行概括。

抄送@rust-lang/lang

@eddyb _require_ 是一个强词,我不得不说不,我们没有严格要求使用内联汇编器。 正如我之前提到的,我们_可以_用纯汇编语言定义过程,单独组装它们,然后通过 FFI 将它们链接到我们的 Rust 程序中。

然而,正如我之前所说,我知道没有任何严肃的操作系统级项目可以做到这一点。 这将意味着大量的样板(阅读:犯错的机会更多),更复杂的构建过程(现在我们很幸运,我们可以通过简单的cargo调用和链接和几乎可以运行的内核从另一端弹出;我们必须在单独的步骤中调用汇编器和链接),并且内联事物的能力急剧下降,等等; 几乎肯定会出现性能下降。

编译器内部函数之类的东西在很多情况下都有帮助,但对于目标 ISA 的监督指令集之类的东西,尤其是更深奥的硬件功能(例如管理程序和安全区功能),通常没有内部函数,我们在no_std 环境。 内在函数通常是不够的; 例如,x86 中断调用约定看起来很酷,但不能让您对陷阱帧中的通用寄存器进行可变访问:假设我采用未定义的指令异常进行仿真,并假设仿真指令返回一个值在 %rax 什么的; 调用约定并没有给我一个很好的方法来将它传递回调用站点,所以我们不得不自己动手。 这意味着在汇编程序中编写我自己的异常处理代码。

所以说实话,不,我们不_require_内联汇编器,但它足够有用,如果没有它几乎是一个非入门者。

@dancrossnyc我特别想避免单独组装,也就是说,无论您如何链​​接,您的项目需要什么样的组装。

在您的情况下,它似乎是主管/管理程序/飞地特权 ISA 子集,对吗?

通常没有内在函数

这是否是必要的,即当通过例如 LLVM 编译为内在调用时,指令是否具有不合理地难以甚至不可能坚持的要求?
或者这仅仅是因为它们被认为过于特殊而无法对大多数开发人员有用?

我们在一个 no_std 环境中

作为记录,供应商内在变量在std::archcore::arch (前者是再出口)。

x86 中断调用约定看起来很酷,但不能让您对陷阱帧中的通用寄存器进行可变访问

cc @rkruppe这可以在 LLVM 中实现吗?

@eddyb正确; 我们需要 ISA 的主管子集。 关于我们的具体用例,恐怕目前我不能说得更多。

这是否是必要的,即当通过例如 LLVM 编译为内在调用时,指令是否具有不合理地难以甚至不可能坚持的要求?
或者这仅仅是因为它们被认为过于特殊而无法对大多数开发人员有用?

在某种程度上两者都是正确的,但总的来说,我会说后者在这里更相关。 有些东西是特定于微体系结构的,并且依赖于特定的处理器包配置。 编译器(例如)将某些内容作为内在的内容公开是否合理,该内容是特权指令子集的一部分 _and_ 以特定处理器版本为条件? 老实说我不知道​​。

作为记录,供应商内在函数在 std::arch 和 core::arch 中(前者是再导出)。

知道这一点真的很好。 谢谢!

编译器(例如)将某些作为特权指令子集的一部分并以特定处理器版本为条件的内在函数公开是否合理? 老实说我不知道​​。

我们已经这样做了。 例如, xsave x86 指令是在core::arch中实现和公开的,并非在所有处理器上都可用,而且大多数都需要特权模式。

@gnzlbg xsave没有特权; 你是说xsaves吗?

我查看了https://rust-lang-nursery.github.io/stdsimd/x86_64/stdsimd/arch/x86_64/index.html以及我在快速扫描中看到的唯一特权指令(我没有做详尽搜索)是xsavesxsaves64xrstorsxrstors64 。 我怀疑这些是内在函数,因为它们属于一般的XSAVE*家族,并且不会在实模式下生成异常,而且有些人想使用 clang/llvm 来编译实模式代码。

@dancrossnyc是的,其中一些就是我的意思(我们在xsave模块中实现了xsavexsavesxsaveopt ...... //github.com/rust-lang-nursery/stdsimd/blob/master/coresimd/x86/xsave.rs)。

这些在core中可用,因此您可以使用它们为 x86 编写操作系统内核。 在用户空间中,它们是无用的 AFAICT(它们总是会引发异常),但我们没有办法在core对此进行区分。 我们只能将它们暴露在core而不是std ,但是由于它们已经稳定,那艘船已经航行了。 谁知道呢,也许有一天某些操作系统会在 ring 0 中运行所有内容,而您可以在那里使用它们...

@gnzlbg我不知道为什么xsaveoptxsave会在用户空间引发异常: xsaves是唯一一个被定义为生成异常的家族(#GP如果 CPL>0),然后仅处于保护模式(SDM vol.1 ch. 13;vol.2C ch. 5 XSAVES)。 xsavexsaveopt可用于实现例如抢占式用户空间线程,因此它们作为内在函数的存在实际上是有意义的。 我怀疑xsaves的内在是因为有人只是添加了xsave系列中的所有内容而没有意识到特权问题(即,假设它可以从用户空间调用),或者有人想调用它从实模式。 后者可能看起来很牵强,但我知道人们正在使用 Clang 和 LLVM 构建实模式固件。

不要误会我的意思; core中 LLVM 内在函数的存在很棒; 如果我永远不必编写那种愚蠢的指令序列来将rdtscp的结果再次转换为有用的格式,我会很高兴。 但是当您编写内核或其他裸机监控类的东西时,当前的内在函数集并不能替代内联汇编器。

@dancrossnyc当我提到xsave我指的是在 CPUID 位 XSAVE、XSAVEOPT、XSAVEC 等后面可用的一些内在函数。其中一些内在函数需要特权模式。

编译器(例如)将某些作为特权指令子集的一部分并以特定处理器版本为条件的内在函数公开是否合理?

我们已经这样做了,它们在稳定的 Rust 中可用。

我怀疑 xsaves 的内在是因为有人只是添加了 xsave 系列中的所有内容而没有意识到特权问题

我添加了这些内在函数。 我们意识到特权问题并决定添加它们,因为对于依赖core的程序作为想要使用这些的操作系统内核来说是完全没问题的,并且它们在用户空间中是无害的(例如,如果你尝试使用它们,您的进程将终止)。

但是当您编写内核或其他裸机监控类的东西时,当前的内在函数集并不能替代内联汇编器。

同意,这就是为什么这个问题仍然存在;)

@gnzlbg抱歉,我并不是想通过对xsave等人的钻研来破坏它。

但是,据我所知,唯一需要特权执行的内在函数是与xsaves相关的内在函数,即便如此,它也不总是有特权的(同样,实模式并不关心)。 很高兴这些在稳定的 Rust 中可用(说真的)。 其他人可能在用户空间中很有用,同样我认为他们在那里很棒。 然而, xsavesxrstors是特权指令集的非常非常小的一部分,并且为两条指令添加内在函数与通常这样做在性质上是不同的,我认为问题仍然是_一般来说_是合适的。 例如,考虑来自 VMX 扩展的VMWRITE指令; 我想一个内在函数会执行诸如执行指令然后“返回” rflags类的事情。 作为内在的东西,这是一种奇怪的专业化的东西。

我认为否则我们在这里是一致的。

FWIW 根据std::arch RFC,我们目前只能将供应商在其 API 中公开的内在函数添加到std::arch 。 对于xsave英特尔在其 C API 上公开它们,这就是为什么它在那里没问题。 如果您需要任何当前未公开的供应商内在函数,请打开一个问题,它是否需要特权模式无关紧要。

如果供应商没有公开它的内在函数,那么std::arch可能不适合它,但是有很多替代方法(内联汇编、全局汇编、调用 C 等)。

抱歉,我理解您是说您为xsave编写的内在函数是指英特尔内在函数; 我之前的评论仍然适用于为什么我认为xsaves是内在的(英特尔编译器编写者的意外或因为有人想要它用于实模式;我觉得前者会很快被注意到,但是固件会做一些奇怪的事情,所以后者根本不会让我感到惊讶)。

无论如何,是的,我认为我们从根本上同意:内在函数不是一切的地方,这就是为什么我们希望看到 asm!() 转向稳定。 正如您昨天所说,听到这方面正在取得进展,我感到非常兴奋,如果我们可以轻轻地推动@Florob将其提高到更接近堆栈顶部的位置,我们将很乐意这样做!

asm!一些其他详细信息和用例:

当您编写操作系统、固件、某些类型的库或某些其他类型的系统代码时,您需要完全访问平台级程序集。 即使我们有内在函数来暴露 Rust 支持的每个架构中的每条指令(我们还没有接近拥有),对于人们经常使用内联汇编进行的一些噱头来说,这

以下是您可以使用内联汇编完成的一部分事情,而其他方式则无法轻松完成。 这些中的每一个都是我见过(或在某些情况下写过)的真实示例,而不是假设。

  • 在单独的 ELF 部分中收集特定指令模式的所有实现,然后在加载代码时,根据您运行的系统的特征在运行时修补该部分。
  • 编写一个跳转指令,其目标在运行时被修补。
  • 发出精确的指令序列(因此您不能指望单个指令的内在函数),以便您可以实现一种模式,仔细处理中间的潜在中断。
  • 发出一条指令,然后跳转到 asm 块的末尾,然后是硬件故障处理程序在指令产生故障时跳转到的故障恢复代码。
  • 发出与汇编器尚不知道的指令相对应的字节序列。
  • 编写一段代码,小心地切换到不同的堆栈,然后调用另一个函数。
  • 调用需要特定寄存器中的参数的汇编例程或系统调用。

+1e6

@eddyb

好的,我将尝试内在方法,看看它需要什么。 你可能是对的,这是对我来说最好的方法。 谢谢!

@joshtriplett 搞定了! 这些是我想到的确切用例。

loop {
   :thumbs_up:
}

我会添加几个其他用例:

  • 以奇怪的架构模式编写代码,例如 BIOS/EFI 调用和 16 位实模式。
  • 使用奇怪/不寻常的寻址模式编写代码(通常在 16 位实模式、引导加载程序等中出现)

@mark-im 绝对! 并概括在我们的两个列表中都有子案例的一点:在调用约定之间进行转换。

我正在关闭 #53118 以支持这个问题,并在此处复制 PR 以作记录。 请注意,这是从 8 月开始的,但简单看一下似乎表明情况没有改变:


内联组装部分需要大修; 在目前的状态下,它意味着行为和语法通常与rustc和 Rust 语言相关联。 几乎整个文档都特定于使用 llvm 工具链的 x86/x86_64 程序集。 需要明确的是,我指的不是汇编代码本身,这显然是特定于平台的,而是指内联汇编的一般体系结构和用法。

当涉及到 ARM 目标时,我没有找到内联汇编行为的权威来源,但根据我的实验和参考ARM GCC 内联汇编文档,以下几点似乎完全不成立:

  • ASM 语法,如 ARM/MIPS(和大多数其他 CISC?)首先使用目标寄存器的 intel-esque 语法。 我理解文档意味着/暗示内联 asm 采用 at&t 语法,该语法被转换为实际平台/编译器特定的语法,我应该只用 ARM 寄存器的名称替换 x86 寄存器的名称。
  • 相关地, intel选项无效,因为它在编译.
  • 改编自 ARM GCC 内联汇编文档(使用arm-none-eabi-*工具链针对thumbv7em-none-eabi进行构建,看来即使是关于内联汇编格式的一些基本假设也是特定于平台的。特别是,它似乎对于 ARM,输出寄存器(第二个宏参数)算作寄存器引用,即$0指的是第一个输出寄存器而不是第一个输入寄存器,就像 x86 llvm 指令的情况一样。
  • 同时,其他特定于编译器的特性_不_存在; 我不能使用对寄存器的命名引用,只能使用索引(例如asm("mov %[result],%[value],ror #1":[result] "=r" (y):[value] "r" (x));无效)。
  • (即使对于 x86/x86_64 目标,内联汇编示例中$0$2的用法也非常令人困惑,因为它没有解释为什么选择这些数字。)

我认为最让我印象深刻的是结束语:

当前执行的 asm! 宏是对 LLVM 的内联汇编器表达式的直接绑定,因此请务必查看他们的文档以及有关 clobbers、约束等的更多信息。

这似乎并非普遍正确。

我理解文档意味着/暗示内联 asm 采用 at&t 语法,该语法被转换为实际平台/编译器特定的语法,我应该只用 ARM 寄存器的名称替换 x86 寄存器的名称。

intel 与 at&t 语法的概念仅存在于 x86 上(尽管可能还有其他我不知道的情况)。 它的独特之处在于它们是两种不同的语言,它们共享同一组助记符来表示同一组二进制代码。 GNU 生态系统已将 at&t 语法建立为 x86 世界的主要默认语法,这就是内联 asm 默认使用的原因。 你错了,它是对 LLVM 的内联汇编器表达式的直接绑定,而后者大多只是将纯文本(处理替换后)转储到文本汇编程序中。 所有这些都与今天的asm!()无关(甚至相关),因为它完全是特定于平台的,并且在 x86 世界之外完全没有意义。

相关地, intel选项无效,因为它在编译时会导致“未知指令”错误。

这是我上面描述的“愚蠢”/简单明文插入的直接结果。 由于错误消息表明.intel_syntax指令不受支持。 这是将 intel-style inline-asm 与 GCC(它发出 att 样式)一起使用的一种古老且众所周知的解决方法:只需在 inline asm 块的开头写入.intel_syntax ,然后编写一些 intel-样式 asm 并最终以.att_syntax终止以将汇编器设置回 att 模式,以便它再次正确处理(以下)编译器生成的代码。 这是一个肮脏的黑客,我记得至少 LLVM 实现有一些奇怪的怪癖很长一段时间,所以你似乎看到了这个错误,因为它最终被删除了。 遗憾的是,这里唯一正确的做法是从 rustc 中删除"intel"选项。

似乎甚至一些关于内联汇编格式的基本假设都是特定于平台的

您的观察完全正确,每个平台都由自己的二进制格式和自己的汇编语言组成。 它们是完全独立的,并且(大部分)未被编译器处理——这是原始汇编程序中编程的全部要点!

我不能使用对寄存器的命名引用,只能使用索引

遗憾的是,rustc 公开的 LLVM 的内联 asm 实现与 GCC(clang 模拟)的实现之间存在很大的不匹配。 如果没有决定如何使用asm!()继续前进,那么改进这一点的动力就很小 - 此外,我很久以前概述了主要选项,所有这些选项都有明显的缺点。 由于这似乎不是一个优先事项,因此您可能至少会被今天的asm!()困住几年。 有一些不错的解决方法:

  • 依靠优化器来生成最佳代码(只需轻轻一点,您通常可以得到您想要的东西,而无需自己编写原始程序集)
  • 使用内在函数,这是另一种非常优雅的解决方案,几乎在所有方面都比内联汇编更好(除非您需要精确控制指令选择和调度)
  • 调用cc从箱build.rs到对象C与内联汇编链接

    • 基本上只需从build.rs调用您喜欢的任何汇编程序,使用 C 编译器可能看起来有点矫枉过正,但可以为您省去与build.rs系统集成的麻烦

这些变通方法适用于除一小部分非常具体的边缘情况之外的所有情况。 如果你击中其中一个(幸运的是我还没有),你就倒霉了。

我同意文档相当乏味,但对于熟悉内联汇编的人来说已经足够了。 如果不是,您可能不应该使用它。 不要误会我的意思 - 您绝对应该随意尝试和学习,但由于asm!()不稳定且被忽视,并且因为有非常好的解决方法,我强烈建议尽可能不要在任何严肃的项目中使用它.

从 build.rs 调用 cc crate 将 C 对象与内联 asm 链接起来

您还可以从build.rs调用cc crate 来构建普通的程序集文件,这提供了最大的控制量。 我强烈建议完全这样做,以防上面的两个“解决方法”不适用于您的用例。

@main--写道:

这些变通方法适用于除一小部分非常具体的边缘情况之外的所有情况。 如果你击中其中一个(幸运的是我还没有),你就倒霉了。

我的意思是,并非完全出于运气。 你只需要使用 Rust 的inline asm 。 我有一个边缘情况,您列出的解决方法都没有在这里涵盖。 正如您所说,如果您熟悉其他编译器的过程,那就没问题了。

(我还有另一个用例:我想有一天教系统编程计算机架构和使用 Rust 而不是 C 的东西。没有内联汇编会让这更尴尬。)

我希望我们将内联汇编作为 Rust 的优先事项,并尽快稳定它。 也许这应该是 Rust 2019 的目标。 我对您之前在好评论中列出的任何解决方案都满意:我可以忍受其中任何一个的问题。 能够内联汇编代码对我来说是在任何地方编写 Rust 而不是 C 的先决条件:我真的需要它稳定。

我希望我们将内联汇编作为 Rust 的优先事项,并尽快稳定它。 也许这应该是 Rust 2019 的目标。

请写一篇 Rust 2019 博客文章并表达这个担忧。 我认为如果我们有足够多的人这样做,我们就可以影响路线图。

为了澄清我上面的评论 - 问题在于文档并没有解释asm!(..)宏的内容被解析/交互的“深度”。 我熟悉 x86 和 MIPS/ARM 汇编,但假设 llvm 有自己的汇编语言格式。 我之前曾为 x86 使用过内联汇编,但不清楚 asm 对 brige C 和 ASM 的混杂到什么程度。 我基于 rust 内联汇编部分中的措辞的假设(现在无效)是 LLVM 有自己的 ASM 格式,该格式旨在模拟 at&t 或 intel 模式下的 x86 汇编,并且看起来必须像所示的 x86 示例。

(帮助我的是研究扩展的宏输出,它清除了正在发生的事情)

我认为该页面上需要减少抽象。 更清楚地说明 LLVM 解析的内容以及直接解释为 ASM 的内容。 哪些部分是生锈所特有的,哪些部分是您运行的硬件所特有的,哪些部分属于将它们固定在一起的胶水。

build.rs调用cc crate 以将 C 对象与内联 asm 链接起来

跨语言 LTO 的最新进展让我想知道是否可以减少这条途径的一些缺点,有效地内联这个“外部程序集 blob”。 (可能不是

build.rs调用cc crate 以将 C 对象与内联 asm 链接起来

跨语言 LTO 的最新进展让我想知道是否可以减少这条途径的一些缺点,有效地内联这个“外部程序集 blob”。

即使这可行,我也不想用 C 编写我的内联程序集。我想用 Rust 编写它。 :-)

我不想用 C 编写我的内联程序集。

您可以直接编译和链接.s.S文件(例如参见这个crate ),在我的书中,它们与 C 相去甚远。:)

如果可以减少这条大道的一些缺点

我相信这目前是不可行的,因为跨语言 LTO 依赖于 LLVM IR,而汇编不会生成它。

我相信这目前是不可行的,因为跨语言 LTO 依赖于 LLVM IR,而汇编不会生成它。

您可以在 LLVM IR 模块中将程序集装入模块级程序集。

有谁知道最近的提案/当前状态是什么? 由于今年的主题是“成熟并完成我们开始的事情”,因此似乎是最终完成asm的绝佳机会。

去年 2 月讨论了新的(待稳定的)语法的模糊计划: https :

根据这些笔记, @joshtriplett@Amanieu 已签约编写 RFC。

新语法的状态如何?

它需要在每晚进行 RFC 和实施

ping @joshtriplett @Amanieu让我知道我是否可以帮助推进这里的事情! 我会尽快与您联系。

@cramertj AFAICT 任何人都可以向前推进,这是

人们可能会尝试将 RFC 之前的内容转变为适当的 RFC 并提交,但我怀疑如果没有实施,这样的 RFC 是否具有说服力。


编辑:明确地说,通过说服我特意指的是像这样的 pre-RFC 的一部分:

此外,还根据需要添加了寄存器类的映射(参见 llvm-constraint 6)

lang-ref 中有许多特定于 arch 的寄存器类。 RFC 不能只是将所有这些都排除在外,并确保它们都像预期的那样工作,或者有意义,或者在 LLVM 中足够“稳定”以从这里公开等等。将受益于一个可以实现的实现试试这些。

#![feature(asm)]是否支持 RISC-V 内联汇编?

据我所知,支持平台上的所有程序集都受支持; 它几乎是对 llvm 编译器的 asm 支持的原始访问。

是的,支持 RISC-V。 特定于架构的输入/输出/clobber 约束类记录在LLVM langref 中

但是,有一个警告 - 如果您需要在输入/输出/clobber 约束中约束单个寄存器,则必须使用架构寄存器名称(x0-x31、f0-f31),而不是 ABI 名称。 在程序集片段本身中,您可以使用任何一种寄存器名称。

作为这些概念的新手,我只能说......整个讨论似乎_愚蠢_。 一种语言(程序集)应该是与它的机器代码进行

我很困惑:

  • 如果您正在编写 asm,难道不应该为您尝试支持的每个架构_和后端_重写它(由具有#[cfg(...)]编写)?
  • 这意味着“语法”问题没有实际意义......只需使用该架构的语法和编译器碰巧使用的后端。
  • Rust 只需要 std 不安全函数就能够将字节放入正确的寄存器中,并为正在编译的任何架构推送/弹出堆栈——同样,这可能必须为每个架构甚至每个后端重写。

我知道向后兼容性是一个问题,但是由于存在大量错误,而且这种情况从未稳定过,也许将其传递给后端会更好。 Rust 不应该试图修复 LLVM 或 gcc 或任何其他人的错误语法。 Rust 致力于为它所针对的架构和编译器发出机器代码……而 asm 基本上就是那个代码!

这里没有进展的原因是没有人花时间解决这个问题。 这不是稳定功能的好理由。

在阅读此线程时,我有了一个想法,不得不发布它。 对不起,如果我在回答一个旧帖子,但我认为这是值得的:

@main-- 说:

这两种优化肯定会产生更好的吞吐量(尤其是在面对超线程时),但不会产生我希望实现的延迟减少。 我最终在那个实验中一路下降到 nasm,但不得不将代码从内在函数重写为普通的 asm 只是不必要的摩擦。 当然,我希望优化器在使用一些高级矢量 API 时处理指令选择或常量折叠之类的事情。 但是当我明确决定使用哪些指令时,我真的不希望编译器乱七八糟。 唯一的选择是内联汇编。

也许不是内联汇编,我们在这里真正需要的是 LLVM 的函数属性,它告诉优化器:“优化吞吐量”、“优化延迟”、“优化二进制大小”。 我知道这个解决方案是上游的,但它不仅会自动解决您的特定问题(通过提供算法的低延迟但其他同构的实现),它还允许 Rust 程序员对性能特征进行更细粒度的控制这对他们来说很重要。

@felix91gr这不能解决需要发出精确指令序列的用例,例如中断处理程序。

@mark-im 当然不是。 这就是为什么我把字面引用! 🙂

我的观点是,即使您可以通过使用内联 asm 功能来解决“编译器以与我需要的方式相反的方式进行优化”(这在他们的情况下是经典的:延迟与吞吐量),也许(并且绝对是 imo)该用例会通过更细粒度的优化控制得到更好的服务:)

鉴于内联汇编即将发生的变化,这个问题的大部分讨论不再相关。 因此,我将关闭此问题,以支持针对我们拥有的每种内联程序集的两个单独的跟踪问题:

  • LLVM 样式内联程序集的跟踪问题 ( llvm_asm ) #70173
  • 内联汇编的跟踪问题 ( asm! ) #72016
此页面是否有帮助?
0 / 5 - 0 等级

相关问题

nikomatsakis picture nikomatsakis  ·  236评论

nikomatsakis picture nikomatsakis  ·  340评论

nikomatsakis picture nikomatsakis  ·  210评论

Leo1003 picture Leo1003  ·  898评论

Mark-Simulacrum picture Mark-Simulacrum  ·  681评论