c - 使用内联汇编循环遍历数组

标签 c gcc assembly inline-assembly

当使用内联汇编循环遍历数组时,我应该使用寄存器修饰符“r”还是内存修饰符“m”?

让我们考虑一个添加两个浮点数组 x 的例子, 和 y并将结果写入 z .通常我会使用内在函数来做到这一点

for(int i=0; i<n/4; i++) {
    __m128 x4 = _mm_load_ps(&x[4*i]);
    __m128 y4 = _mm_load_ps(&y[4*i]);
    __m128 s = _mm_add_ps(x4,y4);
    _mm_store_ps(&z[4*i], s);
}

这是我使用寄存器修饰符“r”提出的内联汇编解决方案
void add_asm1(float *x, float *y, float *z, unsigned n) {
    for(int i=0; i<n; i+=4) {
        __asm__ __volatile__ (
            "movaps   (%1,%%rax,4), %%xmm0\n"
            "addps    (%2,%%rax,4), %%xmm0\n"
            "movaps   %%xmm0, (%0,%%rax,4)\n"
            :
            : "r" (z), "r" (y), "r" (x), "a" (i)
            :
        );
    }
}

这会生成与 GCC 类似的程序集。主要区别在于 GCC 向索引寄存器添加 16 并使用比例为 1,而内联汇编解决方案向索引寄存器添加 4 并使用比例为 4。

我无法为迭代器使用通用寄存器。我必须指定一个,在这种情况下是 rax .是否有一个原因?

这是我使用内存修饰符“m”提出的解决方案
void add_asm2(float *x, float *y, float *z, unsigned n) {
    for(int i=0; i<n; i+=4) {
        __asm__ __volatile__ (
            "movaps   %1, %%xmm0\n"
            "addps    %2, %%xmm0\n"
            "movaps   %%xmm0, %0\n"
            : "=m" (z[i])
            : "m" (y[i]), "m" (x[i])
            :
            );
    }
}

这效率较低,因为它不使用索引寄存器,而是必须将 16 添加到每个数组的基址寄存器。生成的程序集是 (gcc (Ubuntu 5.2.1-22ubuntu2) with gcc -O3 -S asmtest.c ):
.L22
    movaps   (%rsi), %xmm0
    addps    (%rdi), %xmm0
    movaps   %xmm0, (%rdx)
    addl    $4, %eax
    addq    $16, %rdx
    addq    $16, %rsi
    addq    $16, %rdi
    cmpl    %eax, %ecx
    ja      .L22

使用内存修饰符“m”有更好的解决方案吗?有没有办法让它使用索引寄存器? 我问的原因是对我来说使用内存修饰符“m”似乎更合乎逻辑,因为我正在读写内存。此外,使用寄存器修饰符“r”,我从不使用起初对我来说似乎很奇怪的输出操作数列表。

也许有比使用“r”或“m”更好的解决方案?

这是我用来测试的完整代码
#include <stdio.h>
#include <x86intrin.h>

#define N 64

void add_intrin(float *x, float *y, float *z, unsigned n) {
    for(int i=0; i<n; i+=4) {
        __m128 x4 = _mm_load_ps(&x[i]);
        __m128 y4 = _mm_load_ps(&y[i]);
        __m128 s = _mm_add_ps(x4,y4);
        _mm_store_ps(&z[i], s);
    }
}

void add_intrin2(float *x, float *y, float *z, unsigned n) {
    for(int i=0; i<n/4; i++) {
        __m128 x4 = _mm_load_ps(&x[4*i]);
        __m128 y4 = _mm_load_ps(&y[4*i]);
        __m128 s = _mm_add_ps(x4,y4);
        _mm_store_ps(&z[4*i], s);
    }
}

void add_asm1(float *x, float *y, float *z, unsigned n) {
    for(int i=0; i<n; i+=4) {
        __asm__ __volatile__ (
            "movaps   (%1,%%rax,4), %%xmm0\n"
            "addps    (%2,%%rax,4), %%xmm0\n"
            "movaps   %%xmm0, (%0,%%rax,4)\n"
            :
            : "r" (z), "r" (y), "r" (x), "a" (i)
            :
        );
    }
}

void add_asm2(float *x, float *y, float *z, unsigned n) {
    for(int i=0; i<n; i+=4) {
        __asm__ __volatile__ (
            "movaps   %1, %%xmm0\n"
            "addps    %2, %%xmm0\n"
            "movaps   %%xmm0, %0\n"
            : "=m" (z[i])
            : "m" (y[i]), "m" (x[i])
            :
            );
    }
}

int main(void) {
    float x[N], y[N], z1[N], z2[N], z3[N];
    for(int i=0; i<N; i++) x[i] = 1.0f, y[i] = 2.0f;
    add_intrin2(x,y,z1,N);
    add_asm1(x,y,z2,N);
    add_asm2(x,y,z3,N);
    for(int i=0; i<N; i++) printf("%.0f ", z1[i]); puts("");
    for(int i=0; i<N; i++) printf("%.0f ", z2[i]); puts("");
    for(int i=0; i<N; i++) printf("%.0f ", z3[i]); puts("");
}

最佳答案

尽可能避免内联汇编:https://gcc.gnu.org/wiki/DontUseInlineAsm .它阻止了许多优化。但是,如果您真的无法通过编译器来制作您想要的 asm,您可能应该在 asm 中编写整个循环,以便您可以手动展开和调整它,而不是做这样的事情。

您可以使用 r索引的约束。使用 q修饰符来获取 64 位寄存器的名称,因此您可以在寻址模式下使用它。当为 32 位目标编译时,q修饰符选择 32 位寄存器的名称,因此相同的代码仍然有效。
如果你想选择使用哪种寻址模式,你需要自己做,使用带有 r 的指针操作数。约束。
GNU C 内联 asm 语法不假设您读取或写入指针操作数指向的内存。 (例如,也许您在指针值上使用了 inline-asm and)。所以你需要用 "memory" 做一些事情clobber 或内存输入/输出操作数,让它知道你修改了什么内存。一个 "memory" clobber 很容易,但强制除本地人之外的所有内容都被溢出/重新加载。见 Clobbers section in the docs有关使用虚拟输入操作数的示例。
具体来说,一个 "m" (*(const float (*)[]) fptr)将告诉编译器整个数组对象是一个任意长度的输入 .即 asm 不能与任何使用 fptr 的商店重新排序作为地址的一部分(或使用它已知指向的数组)。也适用于 "=m""+m"约束(显然没有 const )。
使用 特定尺寸,如 "m" (*(const float (*)[4]) fptr) 让你告诉编译器你做什么/不读什么。 (或写)。然后它可以(如果另外允许的话)将存储沉入 asm 之后的元素。语句,并将其与您的内联 asm 未读取的任何存储的另一个存储(或执行死存储消除)结合起来。
(有关此问题的完整问答,请参阅 How can I indicate that the memory *pointed* to by an inline ASM argument may be used?。)

m 的另一个巨大好处约束是 -funroll-loops可以工作通过生成具有恒定偏移量的地址。我们自己进行寻址可以防止编译器每 4 次迭代或其他事情进行一次增量,因为 i 的每个源级值需要出现在寄存器中。

这是我的版本,在评论中做了一些调整。这不是最佳的,例如编译器无法有效展开。

#include <immintrin.h>
void add_asm1_memclobber(float *x, float *y, float *z, unsigned n) {
    __m128 vectmp;  // let the compiler choose a scratch register
    for(int i=0; i<n; i+=4) {
        __asm__ __volatile__ (
            "movaps   (%[y],%q[idx],4), %[vectmp]\n\t"  // q modifier: 64bit version of a GP reg
            "addps    (%[x],%q[idx],4), %[vectmp]\n\t"
            "movaps   %[vectmp], (%[z],%q[idx],4)\n\t"
            : [vectmp] "=x" (vectmp)  // "=m" (z[i])  // gives worse code if the compiler prepares a reg we don't use
            : [z] "r" (z), [y] "r" (y), [x] "r" (x),
              [idx] "r" (i) // unrolling is impossible this way (without an insn for every increment by 4)
            : "memory"
          // you can avoid a "memory" clobber with dummy input/output operands
        );
    }
}
Godbolt compiler explorer这个和下面的几个版本的 asm 输出。
您的版本需要声明 %xmm0被破坏,否则当它被内联时你会过得很糟糕。我的版本使用一个临时变量作为从未使用过的仅输出操作数。这为编译器分配寄存器提供了完全的自由。
如果你想避免“内存”破坏,你可以使用虚拟内存输入/输出操作数,如 "m" (*(const __m128*)&x[i])告诉编译器您的函数读取和写入哪个内存。如果你做了类似 x[4] = 1.0; 的事情,这对于确保正确的代码生成是必要的。在运行该循环之前。 (即使你没有写那么简单的东西,内联和常量传播也可以归结为那个。)还要确保编译器不会从 z[] 中读取。在循环运行之前。
在这种情况下,我们得到了可怕的结果:gcc5.x 实际上增加了 3 个额外的指针,因为它决定使用 [reg]寻址模式而不是索引。它不知道内联汇编实际上从未使用约束创建的寻址模式引用那些内存操作数!
# gcc5.4 with dummy constraints like "=m" (*(__m128*)&z[i]) instead of "memory" clobber
.L11:
    movaps   (%rsi,%rax,4), %xmm0   # y, i, vectmp
    addps    (%rdi,%rax,4), %xmm0   # x, i, vectmp
    movaps   %xmm0, (%rdx,%rax,4)   # vectmp, z, i

    addl    $4, %eax        #, i
    addq    $16, %r10       #, ivtmp.19
    addq    $16, %r9        #, ivtmp.21
    addq    $16, %r8        #, ivtmp.22
    cmpl    %eax, %ecx      # i, n
    ja      .L11        #,
r8、r9 和 r10 是内联 asm 块不使用的额外指针。
您可以使用约束告诉 gcc 任意长度的整个数组是输入还是输出:"m" (*(const char (*)[]) pStr) .这将指针转换为指向数组的指针(未指定大小)。见 How can I indicate that the memory *pointed* to by an inline ASM argument may be used?
如果我们想使用索引寻址模式,我们会将所有三个数组的基地址都放在寄存器中,这种形式的约束要求(整个数组的)基地址作为操作数,而不是指向当前内存的指针被操作。
这实际上在循环内没有任何额外的指针或计数器增量的情况下工作: (避免了 "memory" 破坏,但仍然不容易被编译器解开)。
void add_asm1_dummy_whole_array(const float *restrict x, const float *restrict y,
                             float *restrict z, unsigned n) {
    __m128 vectmp;  // let the compiler choose a scratch register
    for(int i=0; i<n; i+=4) {
        __asm__ __volatile__ (
            "movaps   (%[y],%q[idx],4), %[vectmp]\n\t"  // q modifier: 64bit version of a GP reg
            "addps    (%[x],%q[idx],4), %[vectmp]\n\t"
            "movaps   %[vectmp], (%[z],%q[idx],4)\n\t"
            : [vectmp] "=x" (vectmp)
             , "=m" (*(float (*)[]) z)  // "=m" (z[i])  // gives worse code if the compiler prepares a reg we don't use
            : [z] "r" (z), [y] "r" (y), [x] "r" (x),
              [idx] "r" (i) // unrolling is impossible this way (without an insn for every increment by 4)
              , "m" (*(const float (*)[]) x),
                "m" (*(const float (*)[]) y)  // pointer to unsized array = all memory from this pointer
        );
    }
}
这为我们提供了与 "memory" 相同的内部循环破坏者:
.L19:   # with clobbers like "m" (*(const struct {float a; float x[];} *) y)
    movaps   (%rsi,%rax,4), %xmm0   # y, i, vectmp
    addps    (%rdi,%rax,4), %xmm0   # x, i, vectmp
    movaps   %xmm0, (%rdx,%rax,4)   # vectmp, z, i

    addl    $4, %eax        #, i
    cmpl    %eax, %ecx      # i, n
    ja      .L19        #,
它告诉编译器每个 asm 块读取或写入整个数组,因此它可能会不必要地阻止它与其他代码交错(例如,在以低迭代计数完全展开之后)。它不会停止展开,但要求在寄存器中包含每个索引值确实降低了它的效率。这不可能以 16(%rsi,%rax,4) 结束。在同一循环中此块的第二个副本中的寻址模式,因为我们对编译器隐藏了寻址。

带有 m 的版本约束,that gcc can unroll :
#include <immintrin.h>
void add_asm1(float *x, float *y, float *z, unsigned n) {
    // x, y, z are assumed to be aligned
    __m128 vectmp;  // let the compiler choose a scratch register
    for(int i=0; i<n; i+=4) {
        __asm__ __volatile__ (
           // "movaps   %[yi], %[vectmp]\n\t"   // get the compiler to do this load instead
            "addps    %[xi], %[vectmp]\n\t"
            "movaps   %[vectmp], %[zi]\n\t"
          // __m128 is a may_alias type so these casts are safe.
            : [vectmp] "=x" (vectmp)         // let compiler pick a stratch reg
              ,[zi] "=m" (*(__m128*)&z[i])   // actual memory output for the movaps store
            : [yi] "0"  (*(__m128*)&y[i])  // or [yi] "xm" (*(__m128*)&y[i]), and uncomment the movaps load
             ,[xi] "xm" (*(__m128*)&x[i])
              //, [idx] "r" (i) // unrolling with this would need an insn for every increment by 4
        );
    }
}
使用 [yi]作为 +x输入/输出操作数会更简单,但以这种方式编写它可以对内联 asm 中的加载取消注释进行较小的更改,而不是让编译器为我们将一个值放入寄存器。

关于c - 使用内联汇编循环遍历数组,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34244185/

相关文章:

c++ - GCC 4.7 中是否存在错误。 2's implementation of shared_ptr' s(模板化)赋值运算符?

image - 在asm中翻转图像中的第一个像素

assembly - 将数据从一个变量复制到另一个变量

c - 通过 pipe() 从子进程接收到正确结果后程序挂起

c++ - "Recompile with -fPIC"即使在添加 -fPIC 编译标志后错误仍然存​​在

gcc - Eclipse 不会在 Mac 上使用 gcc 4.7

c - ARM GCC 生成的函数序言

c - 为什么下面的 Flex 代码不显示输出?

c - 使用 C 查找 MPG 文件比特率

c - 将字符串分配给C中结构中的元素