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

p3rjfoxz  于 2023-02-23  发布在  其他
关注(0)|答案(1)|浏览(189)

我们认为我们正在X86_64架构上使用GCC(或GCC兼容)编译器,并且eaxebxecxedxlevel是用于指令(如here)的输入和输出的变量(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用作serializing instruction的上下文中,所有这些调用之间有什么区别(例如,指令的输出不会做任何事情)。
  • 这些调用中的某些调用会导致错误吗?
  • 这些调用中的哪一个是最合适的(假设我希望开销尽可能小,但同时希望序列化尽可能“最强”)?
8mmmxcuj

8mmmxcuj1#

首先,lfence的序列化强度可能足以满足您的用例,例如rdtsc。如果您关心性能,请检查并查看是否可以找到lfence足够强大的证据(至少对于您的用例来说)。甚至使用两个mfence; lfence也可能比cpuid更好,例如,如果要在rdtsc之前清空存储缓冲区。
但是lfencemfence都没有在官方技术术语的意义上在整个流水线上进行序列化,这可能对交叉修改代码很重要--丢弃可能在另一个内核的一些存储变得可见之前获取的指令。
2.是的,所有不告诉编译器asm语句写E[A-D]X的代码都是危险的,可能会导致难以调试的问题(即需要使用(哑)输出操作数或错误)。
您需要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 * statement ,内嵌到你使用它们的函数中。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语句是编译器的障碍,无法对加载或存储进行重新排序。
优化器根本不关心第一个字符串字面值中的内容,只关心约束/ clobbers,因此asm volatile("anything" ::: register clobbers, "memory")也是一个仅在编译时使用的内存屏障。
"0"(level)是第一个操作数("=a")的匹配约束,你同样可以写"a"(level),因为在这种情况下编译器没有选择寄存器的选择权;输出约束只能由eax满足。您也可以使用"+a"(eax)作为输出操作数,但是您必须在asm语句之前设置eax=level。对于x87堆栈填充,有时需要匹配约束而不是读写操作数;我想这在一个SO问题中出现过一次,但除了像这样奇怪的东西,优点是可以使用不同的C变量作为输入和输出,或者根本不使用变量作为输入(例如,一个文字常量,或者一个左值(表达式))。
无论如何,告诉编译器提供一个输入可能会导致额外的指令,例如level=0会导致eaxxor-置零。这将是一个指令的浪费,如果它不需要一个置零寄存器用于任何更早的事情。通常异或置零一个输入会打破对前一个值的依赖性。但是这里CPUID的全部意义在于它正在 * 序列化 *,因此无论如何它必须等待所有前面的指令完成执行。如果你不关心输出,甚至不要告诉编译器你的asm语句需要一个输入.编译器使得使用一个未定义/未初始化的值而不产生额外开销变得困难或不可能;有时候,让一个C变量不初始化会导致从堆栈中加载垃圾,或者将寄存器置零,而不是仅仅使用一个寄存器而不先写它。

相关问题