使用 gcc 使用 YMM 指令添加数组

Adding arrays using YMM instructions using gcc

我想在 gcc(AT&T 语法)中 运行 以下代码(Intel 语法)。

; float a[128], b[128], c[128];
; for (int i = 0; i < 128; i++) a[i] = b[i] + c[i];
; Assume that a, b and c are aligned by 32
      xor ecx, ecx              ; Loop counter i = 0
L:    vmovaps ymm0, [b+rcx]     ; Load 8 elements from b
      vaddps ymm0,ymm0,[c+rcx]  ; Add  8 elements from c    
      vmovaps [a+rcx], ymm0     ; Store result in a
      add ecx,32                ; 8 elements * 4 bytes = 32 
      cmp ecx, 512              ; 128 elements * 4 bytes = 512 
      jb L                      ;Loop

代码来自 Optimizing subroutines in assembly language.

到目前为止我写的代码是:

static inline void addArray(float *a, float *b, float *c) {
__asm__ __volatile__ (
    "nop                            \n"
    "xor        %%ecx, %%ecx        \n" //;Loop counter set to 0
    "loop: \n\t"
    "vmovaps    %1, %%ymm0          \n" //;Load 8 elements from b  <== WRONG
    "vaddps     %2, %%ymm0, %%ymm0  \n" //;Add  8 elements from c  <==WRONG
    "vmovaps    %%ymm0, %0          \n" //;Store result in a
    "add        0x20, %%ecx         \n" //;8 elemtns * 4 bytes = 32 (0x20)
    "cmp        0x200,%%ecx         \n" //;128 elements * 4 bytes = 512 (0x200)
    "jb         loop                \n" //;Loop"
    "nop                            \n"
        : "=m"(a)                       //Outputs       
        : "m"(b), "m"(c)                //Inputs
        : "%ecx","%ymm0"                //Modifies ECX and YMM0
    );
}

标记为 "wrong" 的行生成:(gdb 反汇编除外)

0x0000000000000b78 <+19>:   vmovaps -0x10(%rbp),%ymm0
0x0000000000000b7d <+24>:   vaddps -0x18(%rbp),%ymm0,%ymm0

我想得到这样的东西(我猜):

vmovaps -0x10(%rbp,%ecx,%0x8),%ymm0

但我不知道如何将 %ecx 指定为我的索引寄存器。

你能帮帮我吗?

编辑

我试过了(%1,%%ecx):

__asm__ __volatile__ (
    "nop                            \n"
    "xor        %%ecx, %%ecx        \n" //;Loop counter set to 0
    "loop: \n\t"
    "vmovaps    (%1, %%rcx), %%ymm0         \n" //;Load 8 elements from b  <== MODIFIED HERE
    "vaddps     %2, %%ymm0, %%ymm0  \n" //;Add  8 elements from c 
    "vmovaps    %%ymm0, %0          \n" //;Store result in a
    "add        0x20, %%ecx         \n" //;8 elemtns * 4 bytes = 32 (0x20)
    "cmp        0x200,%%ecx         \n" //;128 elements * 4 bytes = 512 (0x200)
    "jb         loop                \n" //;Loop"
    "nop                            \n"
        : "=m"(a)                       //Outputs       
        : "m"(b), "m"(c)                //Inputs
        : "%ecx","%ymm0"                //Modifies ECX and YMM0
    );

我得到了:

inline1.cpp: Assembler messages:
inline1.cpp:90: Error: found '(', expected: ')'
inline1.cpp:90: Error: junk `(%rbp),%rcx)' after expression

我认为不可能将其逐字翻译成 GAS 内联汇编。在AT&T语法中,语法为:

displacement(base register, offset register, scalar multiplier)

这会产生类似于:

movl  -4(%ebp, %ecx, 4), %eax

或者您的情况:

vmovaps  -16(%rsp, %ecx, 0), %ymm0

问题是,当您使用内存约束 (m) 时,内联汇编程序将在您编写 %n 的任何地方发出以下内容(其中 n 是数字input/output):

-16(%rsp)

没有办法将上面的操作变成你真正想要的形式。你可以这样写:

(%1, %%rcx)

但这会产生:

(-16(%rsp),%rcx)

这显然是错误的。无法获取这些括号内 内的偏移寄存器 ,它所属的位置,因为 %n 将整个 -16(%rsp) 作为一个块发出。

当然,这不是真正的问题,因为您编写内联汇编来获得速度,而且从内存加载并没有什么快的。您应该在寄存器中输入,当您对 input/output (r) 使用寄存器约束时,您没有问题。请注意,这将需要稍微修改您的代码

内联汇编的其他问题包括:

  1. 数字文字以 $ 开头。
  2. 指令应具有大小后缀,例如 l 表示 32 位,q 表示 64 位。
  3. 当您通过 a 写入时,您正在破坏内存,因此您应该有一个 memory 破坏。
  4. 开头和结尾的nop指令完全没有意义。他们甚至没有调整分支目标。
  5. 除了 new-line (\n) 之外,每一行都应该真正以制表符 (\t) 结尾,以便在检查反汇编时得到正确对齐.

这是我的代码版本:

void addArray(float *a, float *b, float *c) {
__asm__ __volatile__ (
    "xorl       %%ecx, %%ecx                \n\t" // Loop counter set to 0
    "loop:                                  \n\t"
    "vmovaps    (%1,%%rcx), %%ymm0          \n\t" // Load 8 elements from b
    "vaddps     (%2,%%rcx), %%ymm0, %%ymm0  \n\t" // Add  8 elements from c
    "vmovaps    %%ymm0, (%0,%%rcx)          \n\t" // Store result in a
    "addl       [=16=]x20,  %%ecx               \n\t" // 8 elemtns * 4 bytes = 32 (0x20)
    "cmpl       [=16=]x200, %%ecx               \n\t" // 128 elements * 4 bytes = 512 (0x200)
    "jb         loop"                             // Loop"
        :                                         // Outputs       
        : "r" (a), "r" (b), "r" (c)               // Inputs
        : "%ecx", "%ymm0", "memory"               // Modifies ECX, YMM0, and memory
    );
}

这会导致编译器发出以下内容:

addArray(float*, float*, float*):
        xorl       %ecx, %ecx                
     loop:                                  
        vmovaps    (%rsi,%rcx), %ymm0           # b
        vaddps     (%rdx,%rcx), %ymm0, %ymm0    # c
        vmovaps    %ymm0, (%rdi,%rcx)           # a
        addl       [=17=]x20,  %ecx               
        cmpl       [=17=]x200, %ecx               
        jb         loop
        vzeroupper
        retq

或者,在更熟悉的英特尔语法中:

addArray(float*, float*, float*):
        xor     ecx, ecx
   loop:
        vmovaps ymm0, YMMWORD PTR [rsi + rcx]
        vaddps  ymm0, ymm0, YMMWORD PTR [rdx + rcx]
        vmovaps YMMWORD PTR [rdi + rcx], ymm0
        add     ecx, 32
        cmp     ecx, 512
        jb      loop
        vzeroupper
        ret

在System V 64位调用约定中,前三个参数在rdirsirdx寄存器中传递,因此代码不需要将参数移动到寄存器中——它们已经在那里了。

但是您没有充分利用 input/output 约束。您不需要rcx用作计数器。您也不需要使用 ymm0 作为临时寄存器。如果您让编译器选择使用哪些空闲寄存器,它将使代码更有效率。您也不需要提供明确的破坏列表:

#include <stdint.h>
#include <x86intrin.h>

void addArray(float *a, float *b, float *c) {
  uint64_t temp = 0;
  __m256   ymm;
  __asm__ __volatile__(
      "loop:                        \n\t"
      "vmovaps    (%3,%0), %1       \n\t" // Load 8 elements from b
      "vaddps     (%4,%0), %1, %1   \n\t" // Add  8 elements from c
      "vmovaps    %1, (%2,%0)       \n\t" // Store result in a
      "addl       [=19=]x20,  %0        \n\t" // 8 elemtns * 4 bytes = 32 (0x20)
      "cmpl       [=19=]x200, %0        \n\t" // 128 elements * 4 bytes = 512 (0x200)
      "jb         loop"                   // Loop
    : "+r" (temp), "=x" (ymm)
    : "r" (a), "r" (b), "r" (c)
    : "memory"
    );
}

当然,正如评论中所提到的,整个练习都是在浪费时间。 GAS-style 内联汇编虽然功能强大,但是 非常 很难正确编写(我什至不能 100% 肯定我这里的代码是正确的!),所以你不应该写任何你绝对不需要的使用内联汇编的东西。这 当然 不是必须的情况——编译器会自动优化加法循环:

void addArray(float *a, float *b, float *c) {
  for (int i = 0; i < 128; i++) a[i] = b[i] + c[i];
}

使用 -O2-mavx2,GCC 将其编译为以下内容:

addArray(float*, float*, float*):
        xor     eax, eax
   .L2:
        vmovss  xmm0, DWORD PTR [rsi+rax]
        vaddss  xmm0, xmm0, DWORD PTR [rdx+rax]
        vmovss  DWORD PTR [rdi+rax], xmm0
        add     rax, 4
        cmp     rax, 512
        jne     .L2
        rep ret

嗯,这看起来很眼熟,不是吗?公平地说,它不像您的代码那样矢量化。您可以使用 -O3-ftree-vectorize 来获取它,但您也可以使用 a lot more code generated, so I'd need a benchmark to convince me that it was actually faster and worth the explosion in code size. But most of this is to handle cases where the input isn't aligned—if you indicate that it is aligned and that the pointer is restricted, that solves these problems and improves the code generation substantially。请注意,它 完全 展开循环,以及矢量化加法。