为什么我的 AVX2 水平加法函数没有比非 SIMD 加法快?
Why my AVX2 horizontal addition function is not faster than non-SIMD addition?
我实现了一个内联函数来添加向量的所有元素,但它并不比非 SIMD 加法快。
声明:
#define N 128
#define M N
int __attribute__(( aligned(32)))temp8[8];
__m256i vec;
int __attribute__(( aligned(32))) c_result[N][M];
这是我在向量中添加所有 int 值的两种方法:
首先,非SIMD版本为:
_mm256_store_si256((__m256i *)&temp8[0] , vec);
c_result[i][j]= temp8[0]+temp8[1]+temp8[2]+temp8[3]+temp8[4]+temp8[5]+temp8[6]+temp8[7];
二、AVX2版本:
c_result[i][j] =_mm256_hadd2_epi32(vec);
我是这样实现hadd2的:
// my horizontal addition of epi32
inline int _mm256_hadd2_epi32(__m256i a)
{
__m256i a_hi;
a_hi = _mm256_permute2x128_si256(a, a, 1); //maybe 1 should be 4
a = _mm256_hadd_epi32(a, a_hi);
a = _mm256_hadd_epi32(a, a);
a = _mm256_hadd_epi32(a, a);
return _mm256_extract_epi32(a,0);
}
我使用 gcc
, Linux-mint
, skylake
微架构。
我猜测可能是以下原因:
Skylake 微体系结构中有 4 个 ALU
整数,与受限向量执行单元相比,它们将快速添加它们,特别是对于需要至少一个周期来重新排序元素的排列,然后是一些 hadd
指令。问题是,我是不是遗漏了什么或者没有必要使用 SIMD 添加所有元素?
更新: 我刚刚将 MUL 程序添加到存储库 here 您可以访问矩阵乘法的全部代码。如果我使用非 SIMD 版本,经过的时间将是 201 ns,而使用 SIMD 版本我需要 210 ns。
直觉可能是这一步...
temp8[0]+temp8[1]+temp8[2]+temp8[3]+temp8[4]+temp8[5]+temp8[6]+temp8[7]
是矢量化应该加速的昂贵部分,但它可能是错误的。加法是一个 muop,只要你处理寄存器(而不是内存),你就可以在最近的 x64 机器上每个周期执行 4 个。所以,理论上,你的处理器可以做到这一点...
周期 1.
temp8[0]+temp8[1]
temp8[2]+temp8[3]
temp8[4]+temp8[5]
temp8[6]+temp8[7]
周期 2
(temp8[0]+temp8[1])+(temp8[2]+temp8[3])
(temp8[4]+temp8[5])+(temp8[6]+temp8[7])
并在第 3 周期获得答案,并有剩余容量。 (我们的处理器是超标量的,并且有一个 out-of-order 管道,所以这会神奇地发生。)
矢量化方法可以快多少?你给了我们答案...
a = _mm256_hadd_epi32(a, a_hi);
a = _mm256_hadd_epi32(a, a);
a = _mm256_hadd_epi32(a, a);
我们可以识别 3 个周期......当然,它看起来更便宜,也许......但是 _mm256_hadd_epi32
内在的可能是 PHADD
指令(~3 muops,每两个周期 1 条指令)。重要的一点是处理器不能同时执行多个 _mm256_hadd_epi32
内在函数,而它可以同时执行多个标量加法。因此,你看,哪个更快成为技术问题。
总之,总结一下我的回答......你不应该期望矢量化在这种情况下有帮助(至少没有太大帮助),因为它不利于廉价指令的超标量执行(添加)。
附录。此代码
_mm256_store_si256((__m256i *)&temp8[0] , vec);
c_result[i][j]= temp8[0]+temp8[1]+temp8[2]+temp8[3]+temp8[4]+temp8[5]+temp8[6]+temp8[7];
可能不会按照您的想法编译。让我们把它作为一个函数刷新出来
uint32_t hadd32(__m256i vector) {
uint32_t buffer[sizeof(__m256i)/sizeof(uint32_t)];
_mm256_store_si256((__m256i *)buffer , vector);
uint32_t answer = buffer[0]+buffer[1]+buffer[2]+buffer[3]+buffer[4]+buffer[5]+buffer[6]+buffer[7];
return answer;
}
几个编译器(clang、GCC 7),将其编译为
vpextrd edx, xmm0, 1
vmovd eax, xmm0
add eax, edx
vpextrd edx, xmm0, 2
add eax, edx
vpextrd edx, xmm0, 3
vextracti128 xmm0, ymm0, 0x1
add eax, edx
vmovd edx, xmm0
add eax, edx
vpextrd edx, xmm0, 1
add eax, edx
vpextrd edx, xmm0, 2
add eax, edx
vpextrd edx, xmm0, 3
add eax, edx
我们识别添加的地方,但是临时缓冲区被完全忽略以支持 vpextrd
调用。这里的教训是始终查看生成的程序集。
我实现了一个内联函数来添加向量的所有元素,但它并不比非 SIMD 加法快。
声明:
#define N 128
#define M N
int __attribute__(( aligned(32)))temp8[8];
__m256i vec;
int __attribute__(( aligned(32))) c_result[N][M];
这是我在向量中添加所有 int 值的两种方法:
首先,非SIMD版本为:
_mm256_store_si256((__m256i *)&temp8[0] , vec);
c_result[i][j]= temp8[0]+temp8[1]+temp8[2]+temp8[3]+temp8[4]+temp8[5]+temp8[6]+temp8[7];
二、AVX2版本:
c_result[i][j] =_mm256_hadd2_epi32(vec);
我是这样实现hadd2的:
// my horizontal addition of epi32
inline int _mm256_hadd2_epi32(__m256i a)
{
__m256i a_hi;
a_hi = _mm256_permute2x128_si256(a, a, 1); //maybe 1 should be 4
a = _mm256_hadd_epi32(a, a_hi);
a = _mm256_hadd_epi32(a, a);
a = _mm256_hadd_epi32(a, a);
return _mm256_extract_epi32(a,0);
}
我使用 gcc
, Linux-mint
, skylake
微架构。
我猜测可能是以下原因:
Skylake 微体系结构中有 4 个 ALU
整数,与受限向量执行单元相比,它们将快速添加它们,特别是对于需要至少一个周期来重新排序元素的排列,然后是一些 hadd
指令。问题是,我是不是遗漏了什么或者没有必要使用 SIMD 添加所有元素?
更新: 我刚刚将 MUL 程序添加到存储库 here 您可以访问矩阵乘法的全部代码。如果我使用非 SIMD 版本,经过的时间将是 201 ns,而使用 SIMD 版本我需要 210 ns。
直觉可能是这一步...
temp8[0]+temp8[1]+temp8[2]+temp8[3]+temp8[4]+temp8[5]+temp8[6]+temp8[7]
是矢量化应该加速的昂贵部分,但它可能是错误的。加法是一个 muop,只要你处理寄存器(而不是内存),你就可以在最近的 x64 机器上每个周期执行 4 个。所以,理论上,你的处理器可以做到这一点...
周期 1.
temp8[0]+temp8[1]
temp8[2]+temp8[3]
temp8[4]+temp8[5]
temp8[6]+temp8[7]
周期 2
(temp8[0]+temp8[1])+(temp8[2]+temp8[3])
(temp8[4]+temp8[5])+(temp8[6]+temp8[7])
并在第 3 周期获得答案,并有剩余容量。 (我们的处理器是超标量的,并且有一个 out-of-order 管道,所以这会神奇地发生。)
矢量化方法可以快多少?你给了我们答案...
a = _mm256_hadd_epi32(a, a_hi);
a = _mm256_hadd_epi32(a, a);
a = _mm256_hadd_epi32(a, a);
我们可以识别 3 个周期......当然,它看起来更便宜,也许......但是 _mm256_hadd_epi32
内在的可能是 PHADD
指令(~3 muops,每两个周期 1 条指令)。重要的一点是处理器不能同时执行多个 _mm256_hadd_epi32
内在函数,而它可以同时执行多个标量加法。因此,你看,哪个更快成为技术问题。
总之,总结一下我的回答......你不应该期望矢量化在这种情况下有帮助(至少没有太大帮助),因为它不利于廉价指令的超标量执行(添加)。
附录。此代码
_mm256_store_si256((__m256i *)&temp8[0] , vec);
c_result[i][j]= temp8[0]+temp8[1]+temp8[2]+temp8[3]+temp8[4]+temp8[5]+temp8[6]+temp8[7];
可能不会按照您的想法编译。让我们把它作为一个函数刷新出来
uint32_t hadd32(__m256i vector) {
uint32_t buffer[sizeof(__m256i)/sizeof(uint32_t)];
_mm256_store_si256((__m256i *)buffer , vector);
uint32_t answer = buffer[0]+buffer[1]+buffer[2]+buffer[3]+buffer[4]+buffer[5]+buffer[6]+buffer[7];
return answer;
}
几个编译器(clang、GCC 7),将其编译为
vpextrd edx, xmm0, 1
vmovd eax, xmm0
add eax, edx
vpextrd edx, xmm0, 2
add eax, edx
vpextrd edx, xmm0, 3
vextracti128 xmm0, ymm0, 0x1
add eax, edx
vmovd edx, xmm0
add eax, edx
vpextrd edx, xmm0, 1
add eax, edx
vpextrd edx, xmm0, 2
add eax, edx
vpextrd edx, xmm0, 3
add eax, edx
我们识别添加的地方,但是临时缓冲区被完全忽略以支持 vpextrd
调用。这里的教训是始终查看生成的程序集。