在 AT&T 内联汇编中将 float/double 设置为常数值

Setting a float/double to a constant value in AT&T inline assembly

我正在考虑提高我编写和分析的 C++ 库的运行时性能。我对汇编(和内联汇编)很陌生,有一个非常基本的问题要问。

如何使用内联汇编将 xmm 寄存器(xmm、ymm、zmm 等)的值设置为常量 float 或 double 值?我强烈不希望使用 GCC 的扩展程序集来使代码更易于移植到 MSVC。当使用 -S 编译时,我看到 GCC 使用了一个 .data 部分,但是,我认为我不能在内联代码中使用它。

为简单起见,假设我想在以下 C 代码中实现 foo 函数:

#include <cstdio>

void foo(double *val);
int main(int argc, char **argv) {
   double val = 0.0;

   foo(&val);
   printf("val: %lf\n", val);
   return 0;
}

void foo(double *val) {
   // return *val + 1.0.
   __asm__ (
      "movq -8(%rbp), %rax\n\t"   // move pointer from stack to rax.
      "movq (%rax), %xmm1\n\t"    // dereference pointer and move to xmm1.
      "?????????????"             // somehow move 1.0 to xmm0.
      "addsd %xmm1, %xmm0\n\t"    // add xmm1 to xmm0.
      "movsd %xmm0, (%rax)\n\t"   // move result back val.
   );
 }

我尝试使用 push [=14=]x3ff0000000000000pushq [=15=]x3ff0000000000000 将值移动到堆栈,然后可能将其移动到 xmm0,结果如下:

"pushq [=16=]x3ff0000000000000\n\t" = "Error: operand type mismatch for `push'".

"push [=17=]x3ff00000\n\t" = 该指令的段错误。

如有任何帮助,我们将不胜感激,并提前感谢您的宝贵时间。

您不能使内联汇编代码可移植到 Microsoft 的 C/C++ 编译器,原因有二。首先是 asm 语句的语法差异太大。 Microsoft 的编译器需要 asm { mov rax, [rbp + 8] } 而不是 asm("movq -8(%rbp), %rax\n\t")。二是微软64位编译器不支持内联汇编

所以你不妨正确地使用 GCC 的扩展语法。因为它是你的内联汇编非常脆弱。您不能指望 val 位于 -8(%rbp)。编译器甚至可能不会将其放入堆栈。您也不能假设编译器不会介意您破坏 RAX、XMM0 和 XMM1。

因此,要正确执行此操作,您需要告诉编译器您想要使用哪些变量以及您要丢弃哪些寄存器。另外,您可以让编译器处理将 1.0 加载到 XMM 寄存器中。像这样:

asm ("movq (%0), %%xmm1\n\t"
     "addsd %1, %%xmm1\n\t"
     "movsd %%xmm1, (%0)\n\t"
     : /* no output operands */
     : "r" (val), "x" (1.0)
     : "xmm1", "memory");

"r" (val) 输入操作数告诉编译器将 val 放入通用寄存器,然后将该寄存器名称替换为 %0 在字符串中出现的任何位置。类似地,"x" (1.0) 告诉编译器将 1.0 放入 XMM 寄存器,用它代替 %1。 clobbers 告诉编译器 XMM1 寄存器和内存中的某些内容一起被语句修改了。您可能还会注意到,我已经交换了 ADDSD 上的操作数,因此该语句只修改了一个寄存器。

下面是我编译它时生成的程序集,我在我的计算机上安装了 GCC 版本:

foo:
    pushq   %rbp
    movq    %rsp, %rbp
    movq    %rcx, 16(%rbp)
    movq    16(%rbp), %rax
    movsd   .LC2(%rip), %xmm0

/APP
    movq (%rax), %xmm1
    addsd %xmm0, %xmm1
    movsd %xmm1, (%rax)
/NO_APP

    popq    %rbp
    ret

.LC2:
    .long   0
    .long   1072693248

看来我的 GCC 版本决定将 val 存储在 16(%rbp) 而不是 -8(%rbp) 中。您的代码甚至无法移植到其他版本的 GCC,更不用说 Microsoft 的编译器了。让我们看看在启用优化的情况下编译它时得到的结果:

foo:
    movsd   .LC0(%rip), %xmm0

/APP
    movq (%rcx), %xmm1
    addsd %xmm0, %xmm1
    movsd %xmm1, (%rcx)
/NO_APP

    ret

看看这个函数是多么的短小精悍。编译器已经消除了所有设置堆栈框架的不必要的样板代码。此外,由于 val 被传递给 RCX 中的函数,编译器直接在内联汇编中使用该寄存器。无需将其存储在堆栈中,只需立即将其加载回另一个寄存器即可。

当然,就像你自己的代码一样,none这个是远程兼容微软的编译器的。他们使其兼容的唯一方法是根本不使用内联汇编。幸运的是,这是一个选项,我的意思不仅仅是使用 *val + 1.0。为此,您需要使用 Intel's intrinsics,GCC、Microsoft C/C++ 以及 Clang 和 Intel 自己的编译器都支持它们。这是一个例子:

#include <emmintrin.h>

void foo(double *val) {
    __m128d a = _mm_load_sd(val);
    const double c = 1.0;
    __m128d b = _mm_load_sd(&c);
    a = _mm_add_sd(a, b);
    _mm_store_sd(val, a);
}

我的编译器在没有优化的情况下编译时做了一些可怕的事情,但这是优化后的样子:

foo:
    movsd   (%rcx), %xmm0
    addsd   .LC0(%rip), %xmm0
    movlpd  %xmm0, (%rcx)
    ret

编译器很聪明,知道可以在ADDSD指令中直接使用内存中存储的1.0常量

如果有人对我的问题的确切答案感兴趣,我也会将其张贴在这里,因为我以某种方式设法凭借运气和 trial/error 弄清楚了。这样做的重点是学习简单的组装。

void foo(double *in) {
   __asm__ (
      "movq -8(%rbp), %rax\n\t"
      "movq (%rax), %xmm1\n\t"
      "movq [=10=]x3FF0000000000000, %rbx\n\t" 
      "movq %rbx, %xmm0\n\t"
      "addsd %xmm1, %xmm0\n\t"
      "movsd %xmm0, (%rax)\n\t"
   );
}