我是SSE2和AVX Realm 的新手。我编写以下代码来测试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位 vector 时,似乎没有进一步的改进。
normal elapsed time: 10.5311
normal restrict elapsed time: 8.00338
sse elapsed time: 0.995806
avx elapsed time: 0.973302
我有两个问题。
最佳答案
标量降低了10倍而不是4倍:
您是在标量定时区域内的c[]
中遇到页面错误,因为这是您第一次编写它。 如果您以其他顺序进行测试,则以先到者为准。 该部分是此错误的重复:Why is iterating though `std::vector` faster than iterating though `std::array`?另请参见Idiomatic way of performance evaluation?normal
在数组的5次传递中的第一个传递此费用。较小的阵列和较大的重复计数将使此费用摊销得更多,但更好地进行内存设置或以其他方式填充目标,以便在定时区域之前对目标进行预故障处理。
normal_res
也是标量,但正在写入已经脏了的c[]
。标量比SSE慢8倍,而不是预期的4倍。
您使用了sqrt(double)
而不是sqrtf(float)
或std::sqrt(float)
。在Skylake-X上,这完美地解决了2个吞吐量的额外因素。查看编译器的asm输出on the Godbolt compiler explorer(GCC 7.4假定系统与your last question相同)。我使用了-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仍然为输入<0调用实际的sqrt
以获取errno
设置。它正在执行if (!(0 <= tmp)) goto fallback
,在0 > tmp
上跳跃或无序。 “幸运的是” sqrt足够慢,因此仍然是唯一的瓶颈。转换和比较/分支的乱序执行意味着SQRT单元仍保持100%的时间忙碌。与Skylake-X上的
vsqrtsd
吞吐量(3个周期)相比,vsqrtss
吞吐量(6个周期)要慢2倍,因此,使用两倍的成本将标量吞吐量提高2倍。Skylake-X上的标量sqrt具有与相应的128位ps / pd SIMD版本相同的吞吐量。 因此,每1个数字6个周期作为
double
,而每4个浮点数3个周期作为ps
vector 充分说明了8x因子。 normal
额外的8倍和10倍的减速仅来自页面错误。SSE与AVX sqrt吞吐量
128位
sqrtps
足以获得SIMD div / sqrt单元的全部吞吐量;假设这是一个像您上一个问题一样的Skylake服务器,它的宽度为256位,但未完全流水线化。 CPU可以交替将128位 vector 发送到低半部分或高半部分,以充分利用硬件的全部宽度,即使您仅使用128位 vector 。请参见Floating point division vs floating point multiplication(FP div和sqrt在同一执行单元上运行。)另请参见https://uops.info/或https://agner.org/optimize/上的指令等待时间/吞吐量数字。
add / sub / mul / fma均为512位宽,并且已完全流水线化;如果您想要可以随 vector 宽度缩放的对象,请使用该值(例如,求6阶多项式之类的值)。 div / sqrt是一种特殊情况。
仅当前端出现瓶颈(4 /时钟指令/ uop吞吐量),或者正在执行一堆add / sub / mul / fma时,您才可以期望对SQRT使用256位 vector 会有所帮助也可以使用 vector 。
256位还不错,但是当唯一的计算瓶颈在div / sqrt单元的吞吐量上时,它就没有帮助。
请参阅John McCalpin的答案,以获取有关由于RFO而导致的仅写成本与读+写相同的更多详细信息。
由于每次内存访问的计算量很少,您可能再次/仍然接近瓶颈。即使FP SQRT硬件较宽/较快,实际上您也可能不会使代码运行得更快。取而代之的是,您只需要让核心花费更多时间在等待数据从内存到达时什么都不做。
看来您已经从128位 vector (2x * 4x = 8x)中获得了预期的加速,因此__m128版本显然也不是内存带宽的瓶颈。
每4次内存访问2x sqrt大约与您在in chat发布的代码中所做的
a[i] = sqrt(a[i])
(每加载+存储1x sqrt)相同,但是您没有为此提供任何数字。那避免了页面错误问题,因为它是在初始化数组后就地重写数组。通常,如果您出于某种原因坚持使用这些疯狂的大型数组(甚至不适合L3缓存)尝试获得4x / 8x / 16x SIMD加速,则就地重写数组是个好主意。
内存访问已通过管道传递,并与计算重叠(假定顺序访问,因此预取器可以连续地将其拉入而不必计算下一个地址):更快的计算不会加快整体进度。缓存行以一定的最大固定带宽从内存中到达,同时约有12个缓存行传输在运行中(Skylake中有12个LFB)。或L2“超队列”可以跟踪更多的高速缓存行(也许是16个?),因此L2预取是在CPU内核停滞的地方读取的。
,只要您的计算能跟上该速率,使其更快将在下一高速缓存行到达之前留下更多无所事事的周期。
(也正在发生将存储缓冲区写回L1d,然后驱逐脏线的情况,但是核心等待内存的基本思想仍然有效。)
您可以将其想像成汽车的走走停停的交通:在您的汽车前方打开一个缝隙。更快地缩小差距并不会提高您的平均速度,而只是意味着您必须更快地停止。
如果要了解AVX和AVX512相对于SSE的优势,则需要较小的阵列(和较高的重复计数)。否则每个 vector 将需要大量的ALU工作,例如多项式。
在许多实际问题中,重复使用相同的数据,因此缓存可以正常工作。而且有可能将您的问题分解为在高速缓存中(甚至在加载到寄存器中)时对一个数据块执行多项操作,从而增加计算强度,以充分利用现代CPU的计算与内存平衡。
关于c++ - 与SSE2相比,AVX为什么不能进一步提高性能?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60472794/