使用带有序列化指令的内联汇编

Using inline assembly with serialization instructions

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

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));

首先,lfence 可能与 cpuid 一样强烈序列化,也可能不是。如果您关心性能,请检查并查看是否可以找到 lfence 足够强大的证据(至少对于您的用例而言)。如果 mfencelfence 都不足以在 AMD 和 Intel 上序列化,甚至 也可能比 cpuid 更好。 (我不确定,请参阅我的链接评论)。


2. 是的,所有不告诉编译器 asm 语句写入 E[A-D]X 的都是危险的,可能会导致难以调试的怪异现象。 (即你需要使用(虚拟的)输出操作数或 clobbers)。

您需要 volatile,因为您希望针对序列化的副作用执行 asm 代码,而不是生成输出。

如果您不想将 CPUID 结果用于任何事情(例如,通过序列化 查询某些内容来执行双重任务),您应该简单地列出注册为破坏者,而不是输出,因此您不需要任何 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__

I am wondering what would be the difference between all these calls

首先,其中 none 个是 "calls"。它们是 asm statements,并内联到您使用它们的函数中。 CPUID 本身也不是 "call",尽管我想您可以将其视为调用 CPU 内置的微代码函数。但是按照这个逻辑,每条指令都是 "call",例如mul rcx 在 RAX 和 RCX 中接受输入,returns 在 RDX:RAX 中接受输入。


前三个(以及后一个没有输出,只有 level 输入)在不通知编译器的情况下通过 RDX 破坏 RAX。它将假设这些寄存器仍然保存着它保存在其中的任何内容。它们显然无法使用。


如果您不使用任何输出,

asm("CPUID":"=a"(eax),"=b"(ebx),"=c"(ecx),"=d"(edx):"0"(level):"memory");没有volatile的那个)将优化掉。如果你确实使用了它们,它仍然可以被提升到循环之外。优化器将非 volatile asm 语句视为没有副作用的纯函数。 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" 破坏本身。 带有内存破坏的 (volatile) asm 语句是一个编译器障碍,无法通过它重新排序加载或存储。

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


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

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