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

我正在用AVX计算八点产品。 在我目前的代码中,我做了这样的事情(展开之前):

常春藤桥/桑迪桥

__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); 
}

Haswell的

__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,我认为答案是在这里FLOPS每个周期的沙桥和haswell SSE2 / AVX / AVX2。 我需要展开循环10次。

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

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  ...

所以通过在9个时钟周期后使用8个部分和(4个来自负载,5个来自乘法),我可以在每个时钟周期提交一个AVX加载,一个AVX加法和一个AVX乘法。

我想这意味着在Ivy Bridge和Haswell的32位模式下,这个任务无法达到最大吞吐量,因为32位模式只有8个AVX寄存器?

编辑:关于赏金。 我的主要问题仍然存在。 我想获得上述Ivy Bridge或Haswell函数的最大吞吐量, n可以是大于或等于64的任何值。我认为这只能通过展开来完成(对于Ivy Bridge是8次而对于Haswell是10次) 。 如果你认为这可以用另一种方法完成,那么我们来看看它。 从某种意义上说,这是一个变化,我如何实现每个周期4个FLOP的理论最大值? 但是,除了乘法和加法之外,我正在寻找一个256位加载(或两个128位加载),一个AVX乘法以及每个时钟周期加一个AVX加Ivy Bridge或两个256位加载和两个FMA3指令每个时钟周期。

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

我应该指出,编译器会给出令人误解的结果MSVC和GCC之间在高度优化矩阵复制代码方面的性能差异

下面是我用于Ivy Bridge的当前功能。 这基本上乘以一个64×64矩阵的一个行a所有64×64矩阵的b (我运行这个功能上的每一行64次a获得在矩阵中的全矩阵乘法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:

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

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

  • 我只是在这里回答我自己的问题来添加信息。

    我继续介绍了常春藤桥代码。 当我第一次测试这个在MSVC2012展开超过两个并没有多大帮助。 然而,我怀疑MSVC并没有根据我对MSVC和GCC之间的性能差异优化实现内在函数,以获得高度优化的矩阵复制代码。 所以我使用g++ -c -mavx -O3 -mabi=ms将内核编译到GCC中,将对象转换为COFF64并将其放入MSVC中,现在我得到了三次展开给出了确认Marat Dunkhan答案的最佳结果。

    以下是Xeon E5 1620 @ 3.6GHz MSVC2012的秒数

    unroll    time default            time with GCC kernel
         1    3.7                     3.2
         2    1.8 (2.0x faster)       1.6 (2.0x faster)
         3    1.6 (2.3x faster)       1.2 (2.7x faster)
         4    1.6 (2.3x faster)       1.2 (2.7x faster)
    

    以下是在Linux上使用fma和GCC的i5-4250U的时间( g++ -mavx -mfma -fopenmp -O3 main.cpp kernel_fma.cpp -o sum_fma

    unroll    time
         1    20.3
         2    10.2 (2.0x faster)
         3     6.7 (3.0x faster) 
         4     5.2 (4.0x faster)
         8     2.9 (7.0x faster)
        10     2.6 (7.8x faster)
    

    以下代码适用于Sandy-Bridge / Ivy Bridge。 对于Haswell,请使用例如tmp0 = _mm256_fmadd_ps(a8,b8_1,tmp0)

    kernel.cpp

    #include <immintrin.h>
    
    extern "C" void foo_unroll1(const int n, const float *b, float *c) {      
        __m256 tmp0 = _mm256_set1_ps(0.0f);
        __m256 a8 = _mm256_set1_ps(1.0f);
        for(int i=0; i<n; i+=8) {
            __m256 b8 = _mm256_loadu_ps(&b[i + 0]);
            tmp0 = _mm256_add_ps(_mm256_mul_ps(a8,b8), tmp0);
        }
        _mm256_storeu_ps(c, tmp0);
    }
    
    extern "C" void foo_unroll2(const int n, const float *b, float *c) {
        __m256 tmp0 = _mm256_set1_ps(0.0f);
        __m256 tmp1 = _mm256_set1_ps(0.0f);
        __m256 a8 = _mm256_set1_ps(1.0f);
        for(int i=0; i<n; i+=16) {
            __m256 b8_1 = _mm256_loadu_ps(&b[i + 0]);
            tmp0 = _mm256_add_ps(_mm256_mul_ps(a8,b8_1), tmp0);
            __m256 b8_2 = _mm256_loadu_ps(&b[i + 8]);
            tmp1 = _mm256_add_ps(_mm256_mul_ps(a8,b8_2), tmp1);
        }
        tmp0 = _mm256_add_ps(tmp0,tmp1);
        _mm256_storeu_ps(c, tmp0);
    }
    
    extern "C" void foo_unroll3(const int n, const float *b, float *c) { 
        __m256 tmp0 = _mm256_set1_ps(0.0f);
        __m256 tmp1 = _mm256_set1_ps(0.0f);
        __m256 tmp2 = _mm256_set1_ps(0.0f);
        __m256 a8 = _mm256_set1_ps(1.0f);
        for(int i=0; i<n; i+=24) {
            __m256 b8_1 = _mm256_loadu_ps(&b[i + 0]);
            tmp0 = _mm256_add_ps(_mm256_mul_ps(a8,b8_1), tmp0);
            __m256 b8_2 = _mm256_loadu_ps(&b[i + 8]);
            tmp1 = _mm256_add_ps(_mm256_mul_ps(a8,b8_2), tmp1);
            __m256 b8_3 = _mm256_loadu_ps(&b[i + 16]);
            tmp2 = _mm256_add_ps(_mm256_mul_ps(a8,b8_3), tmp2);
        }
        tmp0 = _mm256_add_ps(tmp0,_mm256_add_ps(tmp1,tmp2));
        _mm256_storeu_ps(c, tmp0);
    }
    
    extern "C" void foo_unroll4(const int n, const float *b, float *c) {      
        __m256 tmp0 = _mm256_set1_ps(0.0f);
        __m256 tmp1 = _mm256_set1_ps(0.0f);
        __m256 tmp2 = _mm256_set1_ps(0.0f);
        __m256 tmp3 = _mm256_set1_ps(0.0f);
        __m256 a8 = _mm256_set1_ps(1.0f);
        for(int i=0; i<n; i+=32) {
            __m256 b8_1 = _mm256_loadu_ps(&b[i + 0]);
            tmp0 = _mm256_add_ps(_mm256_mul_ps(a8,b8_1), tmp0);
            __m256 b8_2 = _mm256_loadu_ps(&b[i + 8]);
            tmp1 = _mm256_add_ps(_mm256_mul_ps(a8,b8_2), tmp1);
            __m256 b8_3 = _mm256_loadu_ps(&b[i + 16]);
            tmp2 = _mm256_add_ps(_mm256_mul_ps(a8,b8_3), tmp2);
            __m256 b8_4 = _mm256_loadu_ps(&b[i + 24]);
            tmp3 = _mm256_add_ps(_mm256_mul_ps(a8,b8_4), tmp3);
        }
        tmp0 = _mm256_add_ps(_mm256_add_ps(tmp0,tmp1),_mm256_add_ps(tmp2,tmp3));
        _mm256_storeu_ps(c, tmp0);
    }
    

    main.cpp中

    #include <stdio.h>
    #include <omp.h>
    #include <immintrin.h>
    
    extern "C" void foo_unroll1(const int n, const float *b, float *c);
    extern "C" void foo_unroll2(const int n, const float *b, float *c);
    extern "C" void foo_unroll3(const int n, const float *b, float *c);
    extern "C" void foo_unroll4(const int n, const float *b, float *c);
    
    int main() {
        const int n = 3*1<<10;
        const int r = 10000000;
        double dtime;
        float *b = (float*)_mm_malloc(sizeof(float)*n, 64);
        float *c = (float*)_mm_malloc(8, 64);
        for(int i=0; i<n; i++) b[i] = 1.0f;
    
        __m256 out;
        dtime = omp_get_wtime();    
        for(int i=0; i<r; i++) foo_unroll1(n, b, c);
        dtime = omp_get_wtime() - dtime;
        printf("%f, ", dtime); for(int i=0; i<8; i++) printf("%f ", c[i]); printf("n");
    
        dtime = omp_get_wtime();    
        for(int i=0; i<r; i++) foo_unroll2(n, b, c);
        dtime = omp_get_wtime() - dtime;
        printf("%f, ", dtime); for(int i=0; i<8; i++) printf("%f ", c[i]); printf("n");
    
        dtime = omp_get_wtime();    
        for(int i=0; i<r; i++) foo_unroll3(n, b, c);
        dtime = omp_get_wtime() - dtime;
        printf("%f, ", dtime); for(int i=0; i<8; i++) printf("%f ", c[i]); printf("n");
    
        dtime = omp_get_wtime();    
        for(int i=0; i<r; i++) foo_unroll4(n, b, c);
        dtime = omp_get_wtime() - dtime;
        printf("%f, ", dtime); for(int i=0; i<8; i++) printf("%f ", c[i]); printf("n");
    }
    
    链接地址: http://www.djcxy.com/p/28779.html

    上一篇: Loop unrolling to achieve maximum throughput with Ivy Bridge and Haswell

    下一篇: Speed up x64 assembler ADD loop