使用带有序列化指令的内联汇编
Using inline assembly with serialization instructions
我们认为我们在 X86_64
架构上使用 GCC
(或 GCC
兼容)编译器,并且 eax
、ebx
、 ecx
、edx
和level
是指令输入输出的变量(unsigned int
或unsigned 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));
- 我不习惯内联汇编语法,我想知道在我只想将
CPUID
用作 serializing instruction 的上下文中,所有这些调用之间有什么区别(例如,指令的输出不会做任何事情)。
- 其中一些调用会导致错误吗?
- 这些调用中的哪一个最适合(考虑到我希望开销尽可能少,但同时 "strongest" 序列化可能)?
首先,lfence
可能与 cpuid
一样强烈序列化,也可能不是。如果您关心性能,请检查并查看是否可以找到 lfence
足够强大的证据(至少对于您的用例而言)。如果 mfence
和 lfence
都不足以在 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
将导致 eax
的 xor
归零。如果它之前不需要任何清零寄存器,这将是一条指令的浪费。通常,对输入进行异或归零会破坏对先前值的依赖,但是 CPUID 的全部意义在于它是 序列化 ,因此它必须等待所有先前的值无论如何都要完成执行的指令。确保 eax
尽早准备好是没有意义的; 如果您不关心输出,甚至不要告诉编译器您的 asm 语句接受输入。编译器很难或不可能在没有开销的情况下使用未定义/未初始化的值;有时让 C 变量未初始化会导致从堆栈加载垃圾,或将寄存器清零,而不是只使用寄存器而不先写入它。
我们认为我们在 X86_64
架构上使用 GCC
(或 GCC
兼容)编译器,并且 eax
、ebx
、 ecx
、edx
和level
是指令输入输出的变量(unsigned int
或unsigned 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));
- 我不习惯内联汇编语法,我想知道在我只想将
CPUID
用作 serializing instruction 的上下文中,所有这些调用之间有什么区别(例如,指令的输出不会做任何事情)。 - 其中一些调用会导致错误吗?
- 这些调用中的哪一个最适合(考虑到我希望开销尽可能少,但同时 "strongest" 序列化可能)?
首先,lfence
可能与 cpuid
一样强烈序列化,也可能不是。如果您关心性能,请检查并查看是否可以找到 lfence
足够强大的证据(至少对于您的用例而言)。如果 mfence
和 lfence
都不足以在 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
将导致 eax
的 xor
归零。如果它之前不需要任何清零寄存器,这将是一条指令的浪费。通常,对输入进行异或归零会破坏对先前值的依赖,但是 CPUID 的全部意义在于它是 序列化 ,因此它必须等待所有先前的值无论如何都要完成执行的指令。确保 eax
尽早准备好是没有意义的; 如果您不关心输出,甚至不要告诉编译器您的 asm 语句接受输入。编译器很难或不可能在没有开销的情况下使用未定义/未初始化的值;有时让 C 变量未初始化会导致从堆栈加载垃圾,或将寄存器清零,而不是只使用寄存器而不先写入它。