与SSE2相比,AVX为什么不能进一步提高性能?

肖恩

我是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

我有两个问题。

  1. AVX为什么不能给我进一步的改进?是因为内存带宽大吗?
  2. 根据我的实验,SSE2的性能比朴素的版本快10倍。这是为什么?基于单精度浮点的128位向量,我预计SSE2只能提高4倍。非常感谢。
彼得·科德斯

标量降低了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个周期)比vsqrtssSkylake-X的吞吐量(3个周期)2倍,因此,使用双倍成本将标量吞吐量提高2倍。

Skylake-X上的标量sqrt具有与相应的128位ps / pd SIMD版本相同的吞吐量。因此,每1个数字有6个周期,doubleps矢量的4个浮点数有3个周期,充分说明了8倍因子。

额外的8倍和10倍的速度降低normal仅来自页面错误。


SSE与AVX sqrt吞吐量

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] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章

为什么并行处理不能进一步加快速度?

为什么git不能进一步减小存储库的大小?

为什么包含需要进一步的依赖?

我可以进一步提高 Rails 应用程序的加载性能吗

为什么按住鼠标按钮输入元素会干扰进一步的鼠标事件?

为什么在继续之后;执行进一步的功能代码被执行?

为什么 VecDeque is_empty 会阻止进一步借用?

什么是迭代器的“进一步过滤”?

进一步的点击会发生什么

我怎么能进一步优化此查询?

为什么SSD不能提高性能?

进一步过滤聚合

进一步解释 keyExtractor

准确性没有进一步提高

我如何进一步理解为什么Go会以这种方式处理错误?

来自具有多个功能的集合的结果不可用于进一步的计算。为什么?

为什么调用indexedDB.deleteDatabase阻止我进行任何进一步的事务?

Elixir:集成测试误报,为什么会失败以及如何防止进一步的误报?

如何进一步优化矩阵乘法的性能?

在Java中使用Jess规则:断言的实例不能用于进一步的推理

可能进一步优化〜20M记录表的`inet'列上的“位图索引扫描”

缓存模运算的结果,但为什么不能提高性能?

有什么方法可以进一步优化Java反射方法调用?

ESC + {:这是什么?在哪里可以进一步了解?

我在什么条件下可以进一步增加5%的折扣?

角色名称存储在哪里以及它的进一步用途是什么?

获得阴影以进一步传播

进一步了解此查询

使用memwatch进一步查找内存泄漏