1

我们认为我们在架构上使用GCC(或GCC兼容)编译器,并且、、和是用于指令输入和输出的变量(或)(如这里)。X86_64eaxebxecxedxlevelunsigned 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用作序列化指令的上下文中(例如,指令的输出不会做任何事情) .
  • 其中一些调用会导致错误吗?
  • 这些调用中的哪一个最适合(假设我希望开销尽可能小,但同时可能是“最强”的序列化)?
4

1 回答 1

2

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


2. 是的,所有不告诉编译器 asm 语句写入 E[AD]X 的都是危险的,并且可能会导致难以调试的怪异。(即您需要使用(虚拟)输出操作数或破坏者)。

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

如果您不想将 CPUID 结果用于任何事情(例如,通过序列化查询某些内容来执行双重任务),您应该简单地将寄存器列为 clobber,而不是输出,因此您不需要任何 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本身。 带有内存破坏器的 ( ) asm 语句是一个编译器屏障,可防止在其上重新排序加载或存储。volatile

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


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

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

于 2018-01-30T14:27:09.590 回答