我是SSE2和AVX领域的新手。我编写以下代码来测试SSE2和AVX的性能。
#include <cmath>
#include <iostream>
#include <chrono>
#include <emmintrin.h>
#include <immintrin.h>
void normal_res(float* __restrict__ a, float* __restrict__ b, float* __restrict__ c, unsigned long N) {
for (unsigned long n = 0; n < N; n++) {
c[n] = sqrt(a[n]) + sqrt(b[n]);
}
}
void normal(float* a, float* b, float* c, unsigned long N) {
for (unsigned long n = 0; n < N; n++) {
c[n] = sqrt(a[n]) + sqrt(b[n]);
}
}
void sse(float* a, float* b, float* c, unsigned long N) {
__m128* a_ptr = (__m128*)a;
__m128* b_ptr = (__m128*)b;
for (unsigned long n = 0; n < N; n+=4, a_ptr++, b_ptr++) {
__m128 asqrt = _mm_sqrt_ps(*a_ptr);
__m128 bsqrt = _mm_sqrt_ps(*b_ptr);
__m128 add_result = _mm_add_ps(asqrt, bsqrt);
_mm_store_ps(&c[n], add_result);
}
}
void avx(float* a, float* b, float* c, unsigned long N) {
__m256* a_ptr = (__m256*)a;
__m256* b_ptr = (__m256*)b;
for (unsigned long n = 0; n < N; n+=8, a_ptr++, b_ptr++) {
__m256 asqrt = _mm256_sqrt_ps(*a_ptr);
__m256 bsqrt = _mm256_sqrt_ps(*b_ptr);
__m256 add_result = _mm256_add_ps(asqrt, bsqrt);
_mm256_store_ps(&c[n], add_result);
}
}
int main(int argc, char** argv) {
unsigned long N = 1 << 30;
auto *a = static_cast<float*>(aligned_alloc(128, N*sizeof(float)));
auto *b = static_cast<float*>(aligned_alloc(128, N*sizeof(float)));
auto *c = static_cast<float*>(aligned_alloc(128, N*sizeof(float)));
std::chrono::time_point<std::chrono::system_clock> start, end;
for (unsigned long i = 0; i < N; ++i) {
a[i] = 3141592.65358;
b[i] = 1234567.65358;
}
start = std::chrono::system_clock::now();
for (int i = 0; i < 5; i++)
normal(a, b, c, N);
end = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed_seconds = end - start;
std::cout << "normal elapsed time: " << elapsed_seconds.count() / 5 << std::endl;
start = std::chrono::system_clock::now();
for (int i = 0; i < 5; i++)
normal_res(a, b, c, N);
end = std::chrono::system_clock::now();
elapsed_seconds = end - start;
std::cout << "normal restrict elapsed time: " << elapsed_seconds.count() / 5 << std::endl;
start = std::chrono::system_clock::now();
for (int i = 0; i < 5; i++)
sse(a, b, c, N);
end = std::chrono::system_clock::now();
elapsed_seconds = end - start;
std::cout << "sse elapsed time: " << elapsed_seconds.count() / 5 << std::endl;
start = std::chrono::system_clock::now();
for (int i = 0; i < 5; i++)
avx(a, b, c, N);
end = std::chrono::system_clock::now();
elapsed_seconds = end - start;
std::cout << "avx elapsed time: " << elapsed_seconds.count() / 5 << std::endl;
return 0;
}
我通过使用g ++编译器如下编译我的程序。
g++ -msse -msse2 -mavx -mavx512f -O2
结果如下。当我使用更高级的256位向量时,似乎没有进一步的改进。
normal elapsed time: 10.5311
normal restrict elapsed time: 8.00338
sse elapsed time: 0.995806
avx elapsed time: 0.973302
我有两个问题。
标量降低了10倍而不是4倍:
c[]
在标量定时区域内会出现页面错误,因为这是您第一次编写它。如果您以不同的顺序进行测试,则以先到者为准。那部分是这个错误的重复:为什么迭代`std :: vector`比迭代`std :: array`更快?另请参阅性能评估的惯用方式?
normal
在数组的5次传递中的第一个传递此费用。较小的阵列和较大的重复计数将使此费用摊销得更多,但更好地进行内存设置或以其他方式填充目标,以便在定时区域之前对目标进行预故障处理。
normal_res
也是标量,但正在写入已经脏了的c[]
。标量比SSE慢8倍,而不是预期的4倍。
您使用sqrt(double)
代替sqrtf(float)
或std::sqrt(float)
。在Skylake-X上,这完美地解决了2个吞吐量的额外因素。在Godbolt编译器浏览器上查看编译器的asm输出(GCC 7.4假定与您的最后一个问题使用的系统相同)。我曾经使用-mavx512f
(这意味着-mavx
和-msse
),并且没有调整选项,希望获得与您所做的相同的代码生成。main
不内联normal_res
,因此我们可以看看它的独立定义。
normal_res(float*, float*, float*, unsigned long):
...
vpxord zmm2, zmm2, zmm2 # uh oh, 512-bit instruction reduces turbo clocks for the next several microseconds. Silly compiler
# more recent gcc would just use `vpxor xmm0,xmm0,xmm0`
...
.L5: # main loop
vxorpd xmm0, xmm0, xmm0
vcvtss2sd xmm0, xmm0, DWORD PTR [rdi+rbx*4] # convert to double
vucomisd xmm2, xmm0
vsqrtsd xmm1, xmm1, xmm0 # scalar double sqrt
ja .L16
.L3:
vxorpd xmm0, xmm0, xmm0
vcvtss2sd xmm0, xmm0, DWORD PTR [rsi+rbx*4]
vucomisd xmm2, xmm0
vsqrtsd xmm3, xmm3, xmm0 # scalar double sqrt
ja .L17
.L4:
vaddsd xmm1, xmm1, xmm3 # scalar double add
vxorps xmm4, xmm4, xmm4
vcvtsd2ss xmm4, xmm4, xmm1 # could have just converted in-place without zeroing another destination to avoid a false dependency :/
vmovss DWORD PTR [rdx+rbx*4], xmm4
add rbx, 1
cmp rcx, rbx
jne .L5
vpxord zmm
每次调用normal
和时,唯一的方法就是将涡轮时钟减少几毫秒(我认为)normal_res
。它不会一直使用512位运算,因此时钟速度可以稍后再次跳回。这可能部分地解释了它不是精确的8倍。
compare / ja是因为您没有使用过,-fno-math-errno
所以GCC仍sqrt
对输入<0的输入进行实际调用以进行errno
设置。它在做if (!(0 <= tmp)) goto fallback
,跳跃0 > tmp
或无序运行。“幸运的是” sqrt足够慢,因此仍然是唯一的瓶颈。转换和比较/分支的乱序执行意味着SQRT单元仍保持100%的时间忙碌。
vsqrtsd
吞吐量(6个周期)比vsqrtss
Skylake-X的吞吐量(3个周期)慢2倍,因此,使用双倍成本将标量吞吐量提高2倍。
Skylake-X上的标量sqrt具有与相应的128位ps / pd SIMD版本相同的吞吐量。因此,每1个数字有6个周期,double
而ps
矢量的4个浮点数有3个周期,充分说明了8倍因子。
额外的8倍和10倍的速度降低normal
仅来自页面错误。
128位sqrtps
足以获得SIMD div / sqrt单元的全部吞吐量;假设这是一个像您上一个问题一样的Skylake服务器,它的宽度为256位,但未完全流水线化。CPU可以交替将128位向量发送到低半部分或高半部分,以充分利用硬件的全部宽度,即使您仅使用128位向量也是如此。请参见浮点除法与浮点乘法(FP div和sqrt在同一执行单元上运行)。
另请参阅https://uops.info/或https://agner.org/optimize/上的指令等待时间/吞吐量数字。
add / sub / mul / fma均为512位宽,并且已完全流水线化;如果您想要可以随矢量宽度缩放的对象,请使用该值(例如,求六阶多项式之类的值)。div / sqrt是一种特殊情况。
仅当前端出现瓶颈(4 /时钟指令/ uop吞吐量),或者正在执行一堆add / sub / mul / fma时,您才可以期望对SQRT使用256位向量会有所帮助也可以使用向量。
256位还不错,但是当唯一的计算瓶颈在div / sqrt单元的吞吐量上时,它就没有帮助。
请参阅John McCalpin的答案,以获取有关由于RFO而导致的仅写成本与读+写相同的更多详细信息。
由于每次内存访问的计算量很少,您可能再次/仍然接近瓶颈。即使FP SQRT硬件更宽/更快,实际上您可能也不会使代码运行得更快。取而代之的是,您只需要让核心花费更多时间在等待数据从内存到达时什么都不做。
看来您已经从128位向量(2x * 4x = 8x)中获得了预期的加速,因此__m128版本显然也不是内存带宽的瓶颈。
每4个内存访问2x sqrt大约与a[i] = sqrt(a[i])
您在chat中发布的代码中所做的操作(每加载+存储1x sqrt )大致相同,但是您没有为此提供任何数字。那避免了页面错误问题,因为它是在初始化数组后就地重写数组。
通常,如果您出于某种原因一直坚持使用这些疯狂的大型数组(甚至不适合L3缓存)尝试获得4x / 8x / 16x SIMD加速,则就地重写数组是个好主意。
内存访问是流水线式的,并且与计算重叠(假定顺序访问,因此预取器可以连续地将其拉入而不必计算下一个地址):更快的计算不会加快整体进度。缓存行以一定的最大最大带宽从内存到达,同时约有12个缓存行传输在运行中(Skylake中有12个LFB)。或L2“超队列”可以跟踪更多的高速缓存行(也许是16个?),因此L2预取是在CPU内核停滞的地方读取的。
只要您的计算能够跟上该速率,使其更快将在下一个缓存行到达之前留下更多的不做任何操作的周期。
(也正在发生将存储缓冲区写回L1d,然后驱逐脏线的情况,但核心等待内存的基本思想仍然有效。)
您可以将其想像成汽车的走走停停的交通方式:您的汽车前方会出现一个缝隙。更快地缩小差距并不会提高您的平均速度,而只是意味着您必须更快地停止。
如果要了解AVX和AVX512相对于SSE的优势,则需要较小的阵列(和较高的重复计数)。否则您将需要为每个向量进行大量的ALU工作,例如多项式。
在许多实际问题中,重复使用相同的数据,因此缓存可以正常工作。而且有可能将您的问题分解为在高速缓存中(甚至在加载到寄存器中)时对一个数据块执行多项操作,从而增加计算强度,以充分利用现代CPU的计算与内存平衡。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句