将内联程序集与序列化指令一起使用

Using inline assembly with serialization instructions

本文关键字:指令 一起 序列化 程序集      更新时间:2023-10-16

我们认为我们在X86_64架构上使用GCC(或GCC兼容)编译器,并且eaxebxecxedxlevel是指令输入和输出的变量(unsigned intunsigned int*)。

asm("CPUID":::);
asm volatile("CPUID":::);
asm volatile("CPUID":::"memory");
asm volatile("CPUID":"=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx)::"memory");
asm volatile("CPUID":"=a"(eax):"0"(level):"memory");
asm volatile("CPUID"::"a"(level):"memory"); // Not sure of this syntax
asm volatile("CPUID":"=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx):"0"(level):"memory");
asm("CPUID":"=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx):"0"(level):"memory");
asm volatile("CPUID":"=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx):"0"(level));
  • 我不习惯内联程序集语法,我想知道所有这些调用之间的区别是什么,在我只想将CPUID用作序列化指令的上下文中(例如,不会对指令的输出执行任何操作)。
  • 其中一些调用会导致错误吗?
  • 这些调用中的哪一个最适合(假设我想要尽可能少的开销,但同时尽可能"最强"的序列化)?

首先,对于您的用例,例如rdtsclfence可能具有足够的强序列化性。 如果您关心性能,请检查并查看是否可以找到lfence足够强大的证据(至少对于您的用例而言)。 甚至可能同时使用这两种mfence; lfence都可能比cpuid更好,如果您想例如在rdtsc之前耗尽存储缓冲区。

但是lfencemfence都没有按照官方技术术语的含义在整个管道上进行序列化,这可能对交叉修改代码很重要 - 丢弃可能在某些存储从另一个核心变得可见之前获取的指令。

<小时 />

2.是的,所有没有告诉编译器asm语句写入E[A-D]X的语句都是危险的,可能会导致难以调试的怪异。 (即您需要使用(虚拟)输出操作数或 clobbers)。您需要volatile,因为您希望执行 asm 代码是为了序列化的副作用,而不是生成输出。

如果你不想将 CPUID 结果用于任何事情(例如,通过序列化查询某些东西来执行双重任务),你应该简单地将寄存器列为 clobbers,而不是输出,这样你就不需要任何 C 变量来保存结果。

// volatile is already implied because there are no output operands
// but it doesn't hurt to be explicit.
// Serialize and block compile-time reordering of loads/stores across this
asm volatile("CPUID"::: "eax","ebx","ecx","edx", "memory");
// the "eax" clobber covers RAX in x86-64 code, you don't need an #ifdef __i386__

我想知道所有这些电话之间有什么区别

首先,这些都不是"呼叫"。 它们是 asm语句,并内联到您使用它们的函数中。 CPUID 本身也不是"调用",尽管我想您可以将其视为调用 CPU 内置的微码函数。 但按照这种逻辑,每条指令都是一个"调用",例如mul rcx在 RAX 和 RCX 中获取输入,并在 RDX:RAX 中返回。


前三个(以及后一个没有输出,只有一个level输入)通过RDX销毁RAX,而不告诉编译器。 它将假定这些登记册仍然保存着它保存的任何内容。 它们显然无法使用。

如果您不使用任何输出,


asm("CPUID":"=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx):"0"(level):"memory");(没有volatile的那个)将优化。如果你确实使用它们,它仍然可以被吊出循环。 优化器将非volatileasm 语句视为没有副作用的纯函数。 https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#index-asm-volatile

它有一个内存破坏器,但(我认为)这并不能阻止它优化,它只是意味着如果/何时/在哪里运行,它可能读取/写入的任何变量都会同步到内存,因此内存内容与 C 抽象机器在这一点上会拥有的内容相匹配。 不过,这可能会排除没有被占用地址的当地人。

asm("" ::: "memory")std::atomic_thread_fence(std::memory_order_seq_cst)非常相似,但请注意,asm语句没有输出,因此隐式volatile这就是为什么它没有被优化掉的原因,而不是因为"memory"的clobber本身。带有内存破坏器的(volatile)asm语句是防止在其上重新排序加载或存储的编译器屏障。

优化器根本不关心第一个字符串文字中的内容,只关心约束/破坏者,因此asm volatile("anything" ::: register clobbers, "memory")也是一个仅编译时的内存屏障。 我假设这就是你想要的,序列化一些内存操作。


"0"(level)是第一个操作数("=a")的匹配约束。 你同样可以写"a"(level),因为在这种情况下,编译器无法选择选择哪个寄存器;输出约束只能由eax满足。 您也可以使用"+a"(eax)作为输出操作数,但随后您必须在 asm 语句之前设置eax=level。 对于 x87 堆栈内容,有时需要匹配约束而不是读写操作数;我认为这在一个SO问题中出现过一次。 但除了像这样奇怪的东西之外,优点是能够使用不同的 C 变量进行输入和输出,或者根本不使用变量进行输入。 (例如,文字常量或左值(表达式))。

无论如何,告诉编译器提供输入可能会导致额外的指令,例如level=0将导致eaxxor归零。 如果它之前不需要任何零寄存器,这将浪费指令。 通常,对输入进行异或归零会破坏对前一个值的依赖,但 CPUID 的全部意义在于它是序列化的,因此无论如何它都必须等待所有先前的指令完成执行。 确保eax尽早做好准备是没有意义的;如果你不关心输出,甚至不要告诉编译器你的 ASM 语句接受输入。 编译器使得在没有开销的情况下使用未定义/未初始化的值变得困难或不可能;有时让 C 变量未初始化将导致从堆栈加载垃圾或将寄存器归零,而不是只使用寄存器而不先写入它。