c++ - 将内联汇编与序列化说明一起使用

标签 c++ gcc x86 inline-assembly cpuid

我们认为我们正在GCC体系结构上使用GCC(或X86_64兼容)编译器,并且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));
  • 我不习惯于内联汇编语法,并且我想知道在我只想将CPUID用作serializing instruction的情况下,所有这些调用之间的区别是什么(例如,指令的输出将不执行任何操作) )。
  • 其中一些调用会导致错误吗?
  • 这些调用中的哪一个是最合适的(假设我希望开销尽可能小,但同时要“最强”的序列化)?
  • 最佳答案

    首先,lfence的序列化程度可能与cpuid一样强,或者也许没有。如果您关心性能,请检查并查看是否可以找到lfence足够强大的证据(至少对于您的用例而言)。如果mfence; lfencecpuid都不足以在AMD和Intel上进行序列化,甚至using both mfence 可能也比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__
    

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



    首先,这些都不是“电话”。它们是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的那个)将优化。而且,如果您确实使用它们,它仍然可以从环路中吊起。优化器将非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),因为在这种情况下,编译器无法选择要选择的寄存器。输出约束只能由eax满足。您也可以使用"+a"(eax)作为输出操作数,但随后必须在asm语句之前设置eax=level。对于x87堆栈,有时需要匹配约束而不是读写操作数。我认为这是一次SO问题。但是除了像这样的怪异事物外,优点是能够对输入和输出使用不同的C变量,或者根本不对输入使用变量。 (例如,文字常量或左值(表达式))。

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

    关于c++ - 将内联汇编与序列化说明一起使用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48522628/

    相关文章:

    c++ - 使用const bool删除调试信息时的编译器优化

    c++ - 可变参数模板和函数的指针 : what compiler is right?

    c - 函数局部变量的字节分配以及 POP 指令是否仅调用一次

    c++ - GCC 和 clang (SFINAE) 之间的重载解析行为差异

    assembly - LAHF(将标志寄存器的低位字节加载到AH)

    linux - x86 汇编字符串缓冲区编号到 ASCII

    c++ - 关于 vector 的调整大小和保留的奇怪内存行为

    c++ - 单线程程序明显使用多核

    c++ - 条件语句(给定)

    linux - 不支持配置 arm-unknown-elf - ARM 工具链