SIMD指令编程demo

本文更新于 2018.10.24

本demo主要使用矩阵相乘, 演示了Intel SSE和AVX内部指令(intrinsics)的显式使用, 并对比了使用gcc和icc(Intel C/C++编译器)使用不同编译选项编译后的代码性能.

完整源码见: https://raw.githubusercontent.com/zzqcn/storage/master/code/c/simd_multiply.c

参考: https://software.intel.com/zh-cn/articles/ticker-tape-part-2

本文的软硬件环境如下:

  • CPU: Intel(R) Xeon(R) CPU E5-2620 v3 @ 2.40GHz
  • 操作系统: CentOS Linux release 7.0.1406
  • 内核: 3.10.0-123.el7.x86_64
  • gcc: 4.8.5 20150623
  • icc: 19.0.0.120 20180804

正常代码

正常代码如下, 直接使用基本算法计算2个矩阵a和b, 结果放在c中:

void multiply(void) {
    unsigned i;
    for(i=0; i<N; i++) {
        c[i] = a[i] * b[i];
    }
}

一次循环计算4次

可以将正常代码改为一次循环内计算4次乘法, 某种情况下可以提升性能:

void multiply(void) {
    unsigned i;
    for(i=0; i<(N & ((~(unsigned)0x3))); i+=4) {
        c[i]   = a[i]   * b[i];
        c[i+1] = a[i+1] * b[i+1];
        c[i+2] = a[i+2] * b[i+2];
        c[i+3] = a[i+3] * b[i+3];
    }
    for(; i<N; i++) {
        c[i] = a[i] * b[i];
    }
}

使用SSE指令

Intel SSE指令通过128bit位宽的专用寄存器, 支持一次操作128bit数据. float是单精度浮点数, 占32bit, 那么可以使用一条SSE指令一次计算4个float数:

void multiply(void) {
    unsigned i;
    __m128 A, B, C;

    for(i=0; i<(N & ((~(unsigned)0x3))); i+=4) {
        A = _mm_load_ps(&a[i]);
        B = _mm_load_ps(&b[i]);
        C = _mm_mul_ps(A, B);
        _mm_store_ps(&c[i], C);
    }
    for(; i<N; i++) {
        c[i] = a[i] * b[i];
    }
}

注意这些SSE指令要求参数中的内存地址必须对齐于16字节边界, 所以可以用以下函数分配内存:

a = (float*) _mm_malloc(N*sizeof(float), 16);
b = (float*) _mm_malloc(N*sizeof(float), 16);
c = (float*) _mm_malloc(N*sizeof(float), 16);

要使用这些intrinsics, 需要包含x86intrin.h头文件.

使用AVX指令

较新的Intel CPU都支持AVX指令集, 它可以一次操作256bit数据, 是SSE的2倍. 使用AVX的代码如下:

void multiply(void) {
    unsigned i;
    __m256 A, B, C;

    for(i=0; i<(N & ((~(unsigned)0x7))); i+=8) {
        A = _mm256_load_ps(&a[i]);
        B = _mm256_load_ps(&b[i]);
        C = _mm256_mul_ps(A, B);
        _mm256_store_ps(&c[i], C);
    }
    for(; i<N; i++) {
        c[i] = a[i] * b[i];
    }
}

AVX指令要求内存地址对齐于32字节边界, 所以内存分配代码改为:

a = (float*) _mm_malloc(N*sizeof(float), 32);
b = (float*) _mm_malloc(N*sizeof(float), 32);
c = (float*) _mm_malloc(N*sizeof(float), 32);

性能对比

我分别使用gcc和icc以默认选项和-O3选项编译了以上3种版本的代码, 其中用gcc编译AVX版代码时需要加-mavx选项.

代码执行时间如下(单位毫秒):

代码版本 gcc gcc -O3 icc icc -O3
原始 946 438 405 404
每次计算4项 780 438 439 442
SSE 680 439 405 406
AVX 545 447 407 406

代码执行时间是连续运行10次取的平均值. 某些时候执行时间起伏时间较大. 下图是根据上表生成的对比图:

../_images/simd_demo.png

由上图可知:

  • 现代编译器在-O3编译时会对代码进行充分优化, 使得本文中的代码无论使不使用SIMD指令性能差距不大
  • gcc编译器默认编译时未对代码进行充分优化, 使得不同算法的代码性能差距较大
  • intel编译器对-O3编译选项不敏感, 总是会自动优化代码

更多参考

  • Intel 64 and IA-32 Architectures Software Developer’s Manual
  • Intel Intrinsics Guide