循环展开以使用 Ivy Bridge 和 Haswell 实现最大吞吐量

2022-01-06 00:00:00 intel c++ sse avx x86

我正在使用 AVX 一次计算八个点积.在我当前的代码中,我做这样的事情(展开之前):

I am computing eight dot products at once with AVX. In my current code I do something like this (before unrolling):

常春藤桥/桑迪桥

__m256 areg0 = _mm256_set1_ps(a[m]);
for(int i=0; i<n; i++) {        
    __m256 breg0 = _mm256_load_ps(&b[8*i]);
    tmp0 = _mm256_add_ps(_mm256_mul_ps(arge0,breg0), tmp0); 
}

哈斯韦尔

__m256 areg0 = _mm256_set1_ps(a[m]);
for(int i=0; i<n; i++) {      
    __m256 breg0 = _mm256_load_ps(&b[8*i]);
    tmp0 = _mm256_fmadd_ps(arge0, breg0, tmp0);
}

我需要为每种情况展开多少次循环以确保最大吞吐量?

对于使用 FMA3 的 Haswell,我认为答案在这里 沙桥和 haswell SSE2/AVX/AVX2 的每个周期的 FLOPS.我需要展开循环 10 次.

For Haswell using FMA3 I think the answer is here FLOPS per cycle for sandy-bridge and haswell SSE2/AVX/AVX2. I need to unroll the loop 10 times.

对于 Ivy Bridge,我认为是 8.这是我的逻辑.AVX 加法延迟为 3,乘法延迟为 5.Ivy Bridge 可以使用不同的端口同时进行一次 AVX 乘法和一次 AVX 加法.使用符号 m 表示乘法,a 表示加法,x 表示无运算以及一个数字来表示部分和(例如,m5 表示与第 5 个部分和的乘法)我可以这样写:

For Ivy Bridge I think it's 8. Here is my logic. The AVX addition has a latency of 3 and the multiplication a latency of 5. Ivy Bridge can do one AVX multiplication and one AVX addition at the same time using different ports. Using the notation m for multiplication, a for addition, and x for no operation as well as a number to indicate the partial sum (e.g. m5 means multiplication with the 5th partial sum) I can write:

port0:  m1  m2  m3  m4  m5  m6  m7  m8  m1  m2  m3  m4  m5  ... 
port1:   x   x   x   x   x  a1  a2  a3  a4  a5  a6  a7  a8  ...

因此,通过在九个时钟周期(四个来自加载,五个来自乘法)后使用 8 个部分求和,我可以在每个时钟周期提交一次 AVX 加载、一次 AVX 加法和一次 AVX 乘法.

So by using 8 partial sums after nine clock cycles (four from the load and five from the multiplication) I can submit one AVX load, one AVX addition and one AVX multiplication every clock cycle.

我猜这意味着不可能在 Ivy Bridge 和 Haswell 的 32 位模式下实现此任务的最大吞吐量,因为 32 位模式只有 8 个 AVX 寄存器?

关于赏金.我的主要问题仍然成立.我想获得上面的 Ivy Bridge 或 Haswell 函数的最大吞吐量,n 可以是大于或等于 64 的任何值.我认为这只能使用展开(Ivy 的八次)桥接和 Haswell 10 次).如果您认为这可以用另一种方法完成,那么让我们看看.从某种意义上说,这是 我如何达到 4 FLOP 的理论最大值的变体每个周期?.但不仅仅是乘法和加法,我正在寻找一个 256 位加载(或两个 128 位加载)、一个 AVX 乘法和一个 AVX 加法,每个时钟周期使用 Ivy Bridge 或两个 256 位加载和两个 FMA3 指令每个时钟周期.

In regards to the bounty. My main questions still hold. I want to get the maximum throughput of either the Ivy Bridge or Haswell functions above, n can be any value greater than or equal to 64. I think this can only be done using unrolling (eight times for Ivy Bridge and 10 times for Haswell). If you think this can be done with another method then let's see it. In some sense this is a variation of How do I achieve the theoretical maximum of 4 FLOPs per cycle?. But instead of only multiplication and addition I'm looking for one 256-bit load (or two 128-bit loads), one AVX multiplication, and one AVX addition every clock cycle with Ivy Bridge or two 256-bit loads and two FMA3 instructions per clock cycle.

我还想知道需要多少个寄存器.对于 Ivy Bridge,我认为它是 10 个.一个用于广播,一个用于负载(由于寄存器重命名只有一个),八个用于八个部分总和.所以我不认为这可以在 32 位模式下完成(事实上,当我在 32 位模式下运行时,性能会显着下降).

I would also like to know how many registers are necessary. For Ivy Bridge I think it's 10. One for the broadcast, one for the load (only one due to register renaming), and eight for the eight partial sums. So I don't think this can be done in 32-bit mode (and indeed when I run in 32-bit mode the performance drops significantly).

我应该指出编译器可能会给出误导性结果 高度优化的矩阵乘法代码在 MSVC 和 GCC 之间的性能差异

I should point out that the compiler can give misleading results Difference in performance between MSVC and GCC for highly optimized matrix multplication code

我用于 Ivy Bridge 的当前功能如下.这基本上将 64x64 矩阵 a 的一行与所有 64x64 矩阵 b 相乘(我在 a 的每一行上运行此函数 64 次在矩阵 c 中得到完整的矩阵乘法.

The current function I'm using for Ivy Bridge is below. This basically multiplies one row of a 64x64 matrix a with all of a 64x64 matrix b (I run this function 64 times on each row of a to get the full matrix multiply in matrix c).

#include <immintrin.h>
extern "C" void row_m64x64(const float *a, const float *b, float *c) {      
    const int vec_size = 8;
    const int n = 64;
    __m256 tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7;
    tmp0 = _mm256_loadu_ps(&c[0*vec_size]);
    tmp1 = _mm256_loadu_ps(&c[1*vec_size]);
    tmp2 = _mm256_loadu_ps(&c[2*vec_size]);
    tmp3 = _mm256_loadu_ps(&c[3*vec_size]);
    tmp4 = _mm256_loadu_ps(&c[4*vec_size]);
    tmp5 = _mm256_loadu_ps(&c[5*vec_size]);
    tmp6 = _mm256_loadu_ps(&c[6*vec_size]);
    tmp7 = _mm256_loadu_ps(&c[7*vec_size]);

    for(int i=0; i<n; i++) {
        __m256 areg0 = _mm256_set1_ps(a[i]);

        __m256 breg0 = _mm256_loadu_ps(&b[vec_size*(8*i + 0)]);
        tmp0 = _mm256_add_ps(_mm256_mul_ps(areg0,breg0), tmp0);    
        __m256 breg1 = _mm256_loadu_ps(&b[vec_size*(8*i + 1)]);
        tmp1 = _mm256_add_ps(_mm256_mul_ps(areg0,breg1), tmp1);
        __m256 breg2 = _mm256_loadu_ps(&b[vec_size*(8*i + 2)]);
        tmp2 = _mm256_add_ps(_mm256_mul_ps(areg0,breg2), tmp2);    
        __m256 breg3 = _mm256_loadu_ps(&b[vec_size*(8*i + 3)]);
        tmp3 = _mm256_add_ps(_mm256_mul_ps(areg0,breg3), tmp3);   
        __m256 breg4 = _mm256_loadu_ps(&b[vec_size*(8*i + 4)]);
        tmp4 = _mm256_add_ps(_mm256_mul_ps(areg0,breg4), tmp4);    
        __m256 breg5 = _mm256_loadu_ps(&b[vec_size*(8*i + 5)]);
        tmp5 = _mm256_add_ps(_mm256_mul_ps(areg0,breg5), tmp5);    
        __m256 breg6 = _mm256_loadu_ps(&b[vec_size*(8*i + 6)]);
        tmp6 = _mm256_add_ps(_mm256_mul_ps(areg0,breg6), tmp6);    
        __m256 breg7 = _mm256_loadu_ps(&b[vec_size*(8*i + 7)]);
        tmp7 = _mm256_add_ps(_mm256_mul_ps(areg0,breg7), tmp7);    
    }
    _mm256_storeu_ps(&c[0*vec_size], tmp0);
    _mm256_storeu_ps(&c[1*vec_size], tmp1);
    _mm256_storeu_ps(&c[2*vec_size], tmp2);
    _mm256_storeu_ps(&c[3*vec_size], tmp3);
    _mm256_storeu_ps(&c[4*vec_size], tmp4);
    _mm256_storeu_ps(&c[5*vec_size], tmp5);
    _mm256_storeu_ps(&c[6*vec_size], tmp6);
    _mm256_storeu_ps(&c[7*vec_size], tmp7);
}

推荐答案

对于 Sandy/Ivy Bridge,您需要在 3 点之前展开:

For Sandy/Ivy Bridge you need to unroll by 3:

  • 只有 FP Add 依赖于循环的前一次迭代
  • FP Add 可以在每个周期发出
  • FP Add 需要三个周期才能完成
  • 因此展开 3/1 = 3 完全隐藏了延迟
  • FP Mul 和 FP Load 不依赖于前一次迭代,您可以依靠 OoO 核心以接近最佳的顺序发出它们.这些指令只有在降低 FP Add 的吞吐量时才会影响展开因子(这里不是这种情况,FP Load + FP Add + FP Mul 可以在每个周期发出).

对于 Haswell,您需要在 10 点之前展开:

For Haswell you need to unroll by 10:

  • 只有 FMA 依赖于循环的前一次迭代
  • FMA 可以在每个周期重复发布(即平均独立指令需要 0.5 个周期)
  • FMA 的延迟为 5
  • 因此展开 5/0.5 = 10 完全隐藏了 FMA 延迟
  • 两个 FP Load 微操作不依赖于前一次迭代,并且可以与 2x FMA 共同发布,因此它们不会影响展开因子.

相关文章