仅包含 3 个元素的两个向量的 AVX 优化相加

Posted

技术标签:

【中文标题】仅包含 3 个元素的两个向量的 AVX 优化相加【英文标题】:AVX-optimized addition of two vectors containing only 3 elements 【发布时间】:2021-06-05 17:41:49 【问题描述】:

我有一些这样的代码:

void add_v3_v3(float r[3], const float a[3])

  r[0] += a[0];
  r[1] += a[1];
  r[2] += a[2];
  

我想将其转换为 AVX 代码,但据我了解,AVX 仅在向量具有四个元素(x、y、z、w)时才有效,而这里我只有三个元素(x、y、 z)。有什么想法吗?

【问题讨论】:

你为什么不忽略第四个值? AVX 具有masked load and store,因此您可以在屏蔽最高 dword 的情况下进行 128 位加载和存储。然后像往常一样使用addps/vaddps进行添加;高位 dword 中会有一个零,您会忽略。 这可以单独计算出来,但是是否有任何相关的上下文?例如,参数是否最初来自内存已经很重要(如果它们不是来自内存,最好避免进行无意义的存储/重新加载) 屏蔽存储在 AMD 上相当慢(屏蔽加载仍然需要额外费用);理想情况下,你可以用一个虚拟的第 4 个元素填充你的数组,这样你就可以进行正常的 4 元素操作。 如果你有很多这些要添加并且它们在内存中是连续的,那么你可以通过加载并将它们添加为三个 4 元素来一次做四个 3 元素向量(12 个浮点数)向量。 【参考方案1】:

将它们向量化为 AVX 的最简单方法是简单地考虑数组,例如

// this does not vectorise
void add_v3_v3(float r[3], const float a[3])

  r[0] += a[0];
  r[1] += a[1];
  r[2] += a[2];
  

// ... but this will
void add_many_v3_v3(float r[], const float a[], int count)

  for(int i = 0; i < count; ++i)
    add_v3_v3(r + i*3, a + i*3);
  

https://godbolt.org/z/6vhnWrh3a

它生成一个内部循环,该循环使用“ps”变体使用 256 位 YMM 寄存器一次处理 8 个浮点数:

.L7:
        vmovups ymm3, YMMWORD PTR [rax+32]
        vmovups ymm4, YMMWORD PTR [rax+64]
        vmovups ymm5, YMMWORD PTR [rax]
        vaddps  ymm1, ymm3, YMMWORD PTR [rdx+32]
        vaddps  ymm0, ymm4, YMMWORD PTR [rdx+64]
        vaddps  ymm2, ymm5, YMMWORD PTR [rdx]
        add     rax, 96
        vmovups YMMWORD PTR [rax-64], ymm1
        vmovups YMMWORD PTR [rax-96], ymm2
        vmovups YMMWORD PTR [rax-32], ymm0
        add     rdx, 96
        cmp     rax, r8
        jne     .L7

或使用 ZMM 寄存器一次 16 个,如果您启用了 AVX512f,例如

.L7:
        vmovups zmm3, ZMMWORD PTR [rax+64]
        vmovups zmm5, ZMMWORD PTR [rax]
        add     rax, 192
        add     rdx, 192
        vmovups zmm4, ZMMWORD PTR [rax-64]
        vaddps  zmm1, zmm3, ZMMWORD PTR [rdx-128]
        vaddps  zmm0, zmm4, ZMMWORD PTR [rdx-64]
        vaddps  zmm2, zmm5, ZMMWORD PTR [rdx-192]
        vmovups ZMMWORD PTR [rax-128], zmm1
        vmovups ZMMWORD PTR [rax-192], zmm2
        vmovups ZMMWORD PTR [rax-64], zmm0
        cmp     r8, rax
        jne     .L7

最糟糕的会是:

typedef __m128 vec3;

这样做只会浪费第 4 次浮点数,这意味着您只会获得 3 倍的性能提升(而不是上面的 8 倍或 16 倍)。嗯,不完全正确...编译器可能能够将 2 个这些 vec3 加法融合到一个 __m256 操作中,给你 6 倍的增长,但它不会像上面的那么好.

然而,这种方法通常失效的地方是点积和叉积。

float dot_v3_v3(float a[3], const float b[3])

  return a[0] * b[0] +
         a[1] * b[1] +
         a[2] * b[2];


一般而言,SIMD 指令喜欢在每个通道上执行相同的操作。点和交叉产品涉及跨通道的操作,因此最终会忽略 SIMD,或者会产生一些稍微次优的 SIMD 使用,并需要大量数据混洗。

避免这种情况的唯一实用方法是将代码转换为数组格式 (SOA) 的结构。例如

struct float16 
   float f[16];
   float operator [](int i) const  return f[i]; 
   float& operator [](int i)  return f[i]; 
;

struct vec3x16 
   float16 x;
   float16 y;
   float16 z;
;

float16 dot_v3_v3_x16(vec3x16 a, vec3x16 b) 
   float16 r;
   for(int i = 0; i < 16; ++i)
   
     r[i] = a.x[i] * b.x[i] +
            a.y[i] * b.y[i] +
            a.z[i] * b.z[i];
   
   return r;

产生:https://godbolt.org/z/qhE5xW91s

dot_v3_v3_x16(vec3x16, vec3x16):
        push    rbp
        mov     rax, rdi
        mov     rbp, rsp
        vmovups zmm1, ZMMWORD PTR [rbp+272]
        vmulps  zmm0, zmm1, ZMMWORD PTR [rbp+80]
        vmovups zmm2, ZMMWORD PTR [rbp+208]
        vfmadd231ps     zmm0, zmm2, ZMMWORD PTR [rbp+16]
        vmovups zmm3, ZMMWORD PTR [rbp+336]
        vfmadd231ps     zmm0, zmm3, ZMMWORD PTR [rbp+144]
        vmovups ZMMWORD PTR [rdi], zmm0
        vzeroupper
        pop     rbp
        ret

内联将删除大部分内容,为您提供大约 3 条指令的 16 个点积(+ 一些未在寄存器中的数据的 movs)。

然而,在结构上,在实践中,到处使用 SOA 代码更难,而不是仅仅坚持标准 vec3 类型。

所以要么完全忽略 AVX,而是尝试从数组的角度进行思考(这将为您提供相当快的结果,并且代码库更易于维护和扩展)

或者,完全以数组格式的结构工作,这将为您提供非常快速的代码,但预计新功能的开发时间会显着增加。 (也请注意,某些 cmath 函数可能无法向量化为 AVX2 或 AVX512)

【讨论】:

***.com/tags/sse/info 有一些关于为 SIMD 构建数据的链接,特别是幻灯片 + 文本:SIMD at Insomniac Games (GDC 2015) 正是谈到了这种情况:人们花费大量精力来“使用 SIMD”和一个 SIMD包含一个 3D 几何向量的向量,然后发现它并没有带来太多的加速。 是的,从历史上看,游戏机的内存速度往往相当慢,而处理器的缓存容量也较小。用未使用的 w 填充 ram 和缓存不会导致快速性能.... ;)

以上是关于仅包含 3 个元素的两个向量的 AVX 优化相加的主要内容,如果未能解决你的问题,请参考以下文章

使用 SIMD AVX 计算两个排序数组的对称差的大小

2 个 AVX-512 向量元素的交错合并 - C 内在

两个向量相加怎么算

随机播放 AVX 寄存器中的元素

两个 16 位整数向量与 C++ 中的 AVX2 的内积

为啥这个 AVX 代码比较慢?