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

时间:2018-01-30 13:28:35

标签: c++ gcc x86 inline-assembly cpuid

我们认为我们在GCC架构上使用GCC(或X86_64 - 兼容)编译器,eaxebx,{{ 1}},ecxedx是指令的输入和输出变量(levelunsigned int)(如here)。

unsigned 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)); 用作serializing instruction的上下文中(例如对指令的输出不会做任何事情)。
  • 这些电话会导致错误吗?
  • 这些调用中哪一个是最合适的(假设我希望尽可能减少开销,但同时“最强”序列化可能)?

1 个答案:

答案 0 :(得分:2)

首先,lfence可能与cpuid一样强烈序列化,也可能不像。如果您关心性能,请检查并查看是否可以找到lfence足够强大的证据(至少对您的用例而言)。如果cpuidmfence都不足以在AMD和英特尔上进行序列化,那么偶数using both mfence; lfence可能会优于lfence。 (我不确定,请看我的链接评论)。

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__
  

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

首先,这些都不是"电话"。他们是语句,并且内联到您使用它们的函数中。 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的人将会优化掉)。如果你使用它们,它仍然可以从循环中提升。非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" clobber本身。 带有内存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会导致xor - 归零eax。如果它之前没有需要归零寄存器,那将浪费指令。通常,对输入进行xor-zeroing会破坏对先前值的依赖性,但这里CPUID的整个点是它的序列化,因此它必须等待所有先前的指令完成执行无论如何。确保eax提早就绪是没有意义的; 如果您不关心输出,请不要告诉编译器您的asm语句需要输入。编译器很难或不可能使用未定义/未初始化的值而没有开销;有时将C变量保留为未初始化将导致从堆栈加载垃圾,或者将寄存器归零,而不是仅使用寄存器而不先写入它。