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

时间:2014-01-13 12:06:00

标签: c++ x86 intel sse avx

我用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 per cycle for sandy-bridge and haswell SSE2/AVX/AVX2。我需要将循环展开10次。

对于Ivy Bridge,我认为它是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的任何值。我认为这只能通过展开来完成(8次为Ivy Bridge和10次) Haswell的时间)。如果您认为可以使用其他方法完成,那么让我们看一下。在某种意义上,这是How do I achieve the theoretical maximum of 4 FLOPs per cycle?的变体。但是,我只是寻找一个256位负载(或两个128位负载),一个AVX乘法,每个时钟周期增加一个AVX,使用Ivy Bridge或两个256位负载和两个FMA3指令每个时钟周期。

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

我应该指出编译器可以提供误导性结果Difference in performance between MSVC and GCC for highly optimized matrix multplication code

我正在使用Ivy Bridge的当前功能如下。这基本上将64x64矩阵a的一行与所有64x64矩阵b相乘(我在a的每一行上运行此函数64次以获得矩阵中的完整矩阵乘法{ {1}})。

c

2 个答案:

答案 0 :(得分:13)

对于Sandy / Ivy Bridge,你需要展开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展开:

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

答案 1 :(得分:6)

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

我继续介绍了Ivy Bridge代码。当我第一次在MSVC2012上测试时,展开两次以上并没有多大帮助。但是,我怀疑MSVC没有根据我在Difference in performance between MSVC and GCC for highly optimized matrix multplication code的观察结果最佳地实现内在函数。所以我使用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)

以下是i5-4250U在Linux中使用fma和GCC的次数(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");
}