在 AT&T 内联装配体中将浮点/双精度设置为常量值

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

本文关键字:双精度 设置 常量 AT      更新时间:2023-10-16

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

如何将xmm寄存器的值(xmm, ymm, zmm等)设置为使用内联汇编的常量浮点数或双精度值?我强烈建议不要使用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: %lfn", val);
   return 0;
}
void foo(double *val) {
   // return *val + 1.0.
   __asm__ (
      "movq -8(%rbp), %raxnt"   // move pointer from stack to rax.
      "movq (%rax), %xmm1nt"    // dereference pointer and move to xmm1.
      "?????????????"             // somehow move 1.0 to xmm0.
      "addsd %xmm1, %xmm0nt"    // add xmm1 to xmm0.
      "movsd %xmm0, (%rax)nt"   // move result back val.
   );
 }

我尝试使用push $0x3ff0000000000000pushq $0x3ff0000000000000将值移动到堆栈,然后可能将其移动到xmm0,结果如下:

"pushq $0x3ff0000000000000nt" = "Error: ' push'操作数类型不匹配".

"push $0x3ff00000nt" =此指令出现分段错误。

任何帮助将是感激的,并提前感谢您的时间。

不能将内联汇编代码移植到Microsoft的C/c++编译器中有两个原因。首先是asm语句的语法差别太大。微软的编译器期望类似于asm { mov rax, [rbp + 8] }而不是asm("movq -8(%rbp), %raxnt")。第二,微软64位编译器不支持内联汇编。

因此,您不妨正确地使用GCC的扩展语法。因为你的内联程序集是非常脆弱的。您不能依赖val位于-8(%rbp)。编译器甚至可能不会把它放在堆栈上。您也不能假定编译器不会介意您丢弃RAX、XMM0和XMM1。

所以你需要告诉编译器你想要使用什么变量,你要丢弃什么寄存器。此外,您还可以让编译器处理将1.0加载到XMM寄存器中的问题。像这样:

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

"r" (val)输入操作数告诉编译器将val放入通用寄存器中,然后将该寄存器名替换到%0中出现在字符串中的位置。类似地,"x" (1.0)告诉编译器将1.0放入XMM寄存器,将其替换为%1。处理器告诉编译器,该语句修改了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,更不用说微软的编译器了。让我们看看当我打开优化功能编译它时得到了什么:

foo:
    movsd   .LC0(%rip), %xmm0
/APP
    movq (%rcx), %xmm1
    addsd %xmm0, %xmm1
    movsd %xmm1, (%rcx)
/NO_APP
    ret

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

当然,就像你自己的代码一样,这些都不能与微软的编译器远程兼容。使其兼容的唯一方法是根本不使用内联程序集。幸运的是,这是一种选择,我不只是指使用*val + 1.0。要做到这一点,你需要使用Intel的intrinsic, 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常量

如果有人对我的问题的确切答案感兴趣,我也把它贴在这里,因为我以某种方式设法弄清楚了纯粹的运气和试验/错误。这样做的目的是为了学习简单的组装。

void foo(double *in) {
   __asm__ (
      "movq -8(%rbp), %raxnt"
      "movq (%rax), %xmm1nt"
      "movq $0x3FF0000000000000, %rbxnt" 
      "movq %rbx, %xmm0nt"
      "addsd %xmm1, %xmm0nt"
      "movsd %xmm0, (%rax)nt"
   );
}