繁体   English   中英

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

[英]Why does not AVX further improve the performance compared with SSE2?

我是 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 的执行速度比 naive 版本快 10 倍。 这是为什么? 我预计 SSE2 基于其 128 位向量相对于单精度浮点只能快 4 倍。 非常感谢。

这里有几个问题......

  1. 内存带宽很可能对这些阵列大小很重要——下面有更多说明。
  2. SSE 和 AVX 平方根指令的吞吐量可能不是您对处理器的期望——更多注释如下。
  3. 第一个测试(“正常”)可能比预期的要慢,因为输出数组在测试的计时部分被实例化(即,创建了虚拟到物理映射)。 (只需在初始化 a 和 b 的循环中用零填充 c 即可解决此问题。)

内存带宽注意事项:

  • 当 N = 1<<30 和 float 变量时,每个数组为 4GiB。
  • 每个测试读取两个数组并写入第三个数组。 第三个数组在被覆盖之前也必须从内存中读取——这被称为“写分配”或“所有权读”。
  • 因此,您在每个测试中读取 12 GiB 并写入 4 GiB。 因此,SSE 和 AVX 测试对应于 ~16 GB/s 的 DRAM 带宽,接近最近处理器上单线程操作通常看到的范围的高端。

指令吞吐量说明:

  • x86 处理器上指令延迟和吞吐量的最佳参考是https://www.agner.org/optimize/ 中的“instruction_tables.pdf”
  • Agner 将“互惠吞吐量”定义为当处理器被赋予相同类型的独立指令的工作负载时,每条退役指令的平均周期数。
  • 例如,对于 Intel Skylake 内核,SSE 和 AVX SQRT 的吞吐量是相同的:
  • SQRTPS (xmm) 1/吞吐量 = 3 --> 每 3 个周期 1 条指令
  • VSQRTPS (ymm) 1/吞吐量 = 6 --> 每 6 个周期 1 条指令
  • 平方根的执行时间预计为 (1<<31) 平方根 / 每条 SSE SQRT 指令 4 个平方根 * 每条 SSE SQRT 指令 3 个周期 / 3 GHz = 0.54 秒(随机假设处理器频率)。
  • “normal”和“normal_res”情况的预期吞吐量取决于生成的汇编代码的细节。

标量是 10 倍而不是 4 倍慢:

您在标量定时区域内的c[]遇到页面错误,因为这是您第一次编写它。 如果您以不同的顺序进行测试,无论哪个先进行,都会付出巨大的代价。 那部分是这个错误的重复: 为什么迭代 `std::vector` 比迭代 `std::array` 快? 另见绩效评估的惯用方式?

normal在数组的 5 次传递中的第一次中支付此成本。 较小的阵列和较大的重复计数会进一步分摊这一点,但最好先 memset 或以其他方式填充您的目的地,以便在定时区域之前对其进行预故障。


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仅在每次调用normalnormal_res开始时将涡轮时钟降低几毫秒(我认为)。 它不会继续使用 512 位操作,因此时钟速度可以稍后再次跳回。 这可能部分解释了它不完全是8 倍。

compare / ja 是因为你没有使用-fno-math-errno所以 GCC 仍然调用实际的sqrt for input < 0 来errno 它正在执行if (!(0 <= tmp)) goto fallback ,跳转到0 > tmp或无序。 “幸运的是” sqrt 足够慢,它仍然是唯一的瓶颈。 转换和比较/分支的乱序执行意味着 SQRT 单元在大约 100% 的时间内仍然处于忙碌状态。

vsqrtsd吞吐量(6 个周期)比 Skylake-X 上的vsqrtss吞吐量(3 个周期)慢 2 vsqrtss ,因此使用 double 的成本是标量吞吐量的 2 倍。

Skylake-X 上的标量 sqrt 与相应的 128 位 ps/pd SIMD 版本具有相同的吞吐量。 因此,每 1 个数字 6 个周期作为double精度数,而每 4 个浮点数为 3 个周期作为ps向量完全解释了 8x 因子。

normal下额外的 8 倍与 10 倍的减速只是由于页面错误。


SSE 与 AVX sqrt 吞吐量

128 位sqrtps足以获得 SIMD div/sqrt 单元的全部吞吐量 假设这是一个 Skylake 服务器,就像你上一个问题一样,它有 256 位宽,但没有完全流水线化。 即使您只使用 128 位向量,CPU 也可以交替将 128 位向量发送到低半或高半,以利用整个硬件宽度。 请参阅浮点除法与浮点乘法(FP div 和 sqrt 在同一执行单元上运行。)

另请参阅https://uops.info/https://agner.org/optimize/上的指令延迟/吞吐量数字。

add/sub/mul/fma 都是 512 位宽且完全流水线化; 如果您想要可以随矢量宽度缩放的东西,请使用它(例如评估 6 阶多项式或其他东西)。 div/sqrt 是一个特例。

只有在前端遇到瓶颈(4/时钟指令/uop 吞吐量),或者如果您正在执行一堆加/减/多/ fma 时,您才会期望使用 256 位向量进行 SQRT也可以使用向量。

256 位并不,但当唯一的计算瓶颈在于 div/sqrt 单元的吞吐量时,它无济于事。


由于 RFO,有关只写成本与读+写成本大致相同的更多详细信息,请参阅 John McCalpin 的回答。

由于每次内存访问的计算量如此之少,您可能会再次/仍然接近内存带宽瓶颈。 即使 FP SQRT 硬件更宽/更快,您在实践中也可能不会让您的代码运行得更快。 相反,您只是让核心在等待数据从内存到达时花费更多时间什么都不做。

似乎您从 128 位向量 (2x * 4x = 8x) 中获得了预期的加速,因此显然 __m128 版本在内存带宽上也没有瓶颈。

每 4 次内存访问 2x sqrt 与您在 chat 中发布的代码中所做的a[i] = sqrt(a[i]) (每次加载 + 存储 1x sqrt a[i] = sqrt(a[i])大致相同,但您没有给出任何数字。 那个避免了页面错误问题,因为它在初始化后就地重写了一个数组。

一般来说,如果您出于某种原因一直坚持尝试使用这些甚至无法放入 L3 缓存的超大数组来获得 4x / 8x / 16x SIMD 加速,那么就地重写数组是一个好主意。


内存访问是流水线式的,并与计算重叠(假设顺序访问,因此预取器可以连续拉入它而无需计算下一个地址):更快的计算不会加快整体进度。 缓存线以某个固定的最大带宽从内存到达,同时传输约 12 个缓存线(Skylake 中有 12 个 LFB)。 或者,L2“超级队列”可以跟踪比这更多的缓存行(可能是 16 条?),因此 L2 预取在 CPU 内核停止的位置之前读取。

只要你的计算能跟上这个速度,让它更快只会在下一个缓存行到达之前留下更多的无操作循环。

(存储缓冲区写回 L1d 然后驱逐脏行也发生了,但核心等待内存的基本思想仍然有效。)


你可以把它想象成汽车中走走停停的交通:在你的车前面开了一个缺口。 更快地缩小差距不会让你获得任何平均速度,这只意味着你必须更快地停下来。


如果您想看到 AVX 和 AVX512 相对于 SSE 的优势,您将需要更小的阵列(和更高的重复次数)。 或者每个向量需要大量的 ALU 工作,比如多项式。

在许多实际问题中,重复使用相同的数据,因此缓存工作。 并且可以将您的问题分解为在高速缓存中(甚至在寄存器中加载)时对一个数据块执行多项操作,以增加足够的计算强度以利用现代 CPU 的计算与内存平衡.

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM