AVX Instruction Introduction

Posted by Zhang Jian on November 19, 2022

简介

SIMD(Single Instruction Multiple Data)是指一条指令能够操作多个数据,是对CPU指令的扩展,主要用来进行小数据的并行操作。

Intel最初支持SIMD的指令集是1996年集成在Pentium里的MMX(Multi-Media Extension,多媒体扩展),它的主要目标是为了支持MPEG视频解码。Intel在1999年Pentium3中推出了SSE(Streaming SIMD Extensions,流式SIMD扩展),是继MMX的扩展指令集,主要用于3D图形计算。Intel在2008年3月份提出了AVX指令集(Advanced Vector Extension,高级向量扩展),它是SSE延伸架构,将SSE中的16个128位XMM寄存器扩展位16个256位YMM寄存器,增加了一倍的运算效率。 (CSAPP)

实现

使用avx指令加速代码主要有两种方式:

自动向量化:intel C++ 编译器进行自动向量化,需要使用 -xhost 编译选项。在 gcc 编译器中的对应选项为 -march=native。开启该选项后,编译器会自动根据 CPU 支持的指令集进行向量化。

手动向量化:主要使用intel intrinsics中的指令进行代码程序的编写

自动向量化

对于代码的指定部分进行向量化可以加上编译宏。

1
2
3
4
5
6
7
void auto(float* a, float* b, int* idx) {
    #pragma ivdep
    #pragma vector always
    for (int j = 0; j < 1024; ++ j) {
        a[idx[j]]   += b[j];
    }
}

速度比较。当使用icc -O3 -xhost的编译指令时,运行100m次,相较于g++ -O3,向量化的编译方式能够获得2倍左右的速度提升。

观察输出文件的汇编代码,可以看到向量化指令的使用:

402ee0:       c4 81 7c 10 14 88       vmovups (%r8,%r9,4),%ymm2
402ee6:       c4 81 7c 10 64 88 20    vmovups 0x20(%r8,%r9,4),%ymm4
402eed:       c5 fc 57 c0             vxorps %ymm0,%ymm0,%ymm0
402ef1:       62 f1 7d 08 74 c8       vpcmpeqb %xmm0,%xmm0,%k1
402ef7:       c5 f4 57 c9             vxorps %ymm1,%ymm1,%ymm1
402efb:       62 f2 7d 29 92 04 97    vgatherdps (%rdi,%ymm2,4),%ymm0
402f02:       62 f1 7d 08 74 d0       vpcmpeqb %xmm0,%xmm0,%k2
402f08:       62 f1 7d 08 74 d8       vpcmpeqb %xmm0,%xmm0,%k3
402f0e:       62 f1 7d 08 74 e0       vpcmpeqb %xmm0,%xmm0,%k4
402f14:       c4 a1 7c 58 1c 8e       vaddps (%rsi,%r9,4),%ymm0,%ymm3
402f1a:       62 f2 7d 2a 92 0c a7    vgatherdps (%rdi,%ymm4,4),%ymm1
402f21:       c4 a1 74 58 6c 8e 20    vaddps 0x20(%rsi,%r9,4),%ymm1,%ymm5
402f28:       49 83 c1 10             add    $0x10,%r9
402f2c:       62 f2 7d 2b a2 1c 97    vscatterdps %ymm3,(%rdi,%ymm2,4)
402f33:       62 f2 7d 2c a2 2c a7    vscatterdps %ymm5,(%rdi,%ymm4,4)

手动向量化

下面是相同代码的手动向量化的实现:

1
2
3
4
5
6
7
8
9
10
11
12
void core_intrinsics(std::vector<float>& a, std::vector<float>& b, int* idx) {
    for (int j = 0; j < 1024; j += 16) {

        __m512i _idx = _mm512_load_epi32(idx + j);
        __m512 _b512 = _mm512_load_ps(b.data() + j);
        __m512 _a512 = _mm512_i32gather_ps(_idx, a.data(), 4);

        __m512 _res = _mm512_add_ps(_a512, _b512);
        _mm512_i32scatter_ps(a.data(), _idx, _res, 4);

    }
}

有兴趣的可以自己尝试下,比较两者方法的速度以及背后的原因。

结论

通过上面的简单实验,我们初步认知了向量化的两种实现方式,但是如何能更好的利用avx指令的优势,需要考虑具体的代码。影响性能的核心因素是memory bound还是compute bound,代码程序是否能实现指令上的并行,代码的关键路径是什么等等都是我们需要考虑的。