仅包含 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 优化相加的主要内容,如果未能解决你的问题,请参考以下文章