SIMD数据级并行处理

分享:  

Q: 那,接下来需要看下下面几个问题:什么是SIMD呢?SIMD最初是用来解决什么问题的呢?SIMD在JSON编码、解析中可以用来做什么呢?simdjson正确使用SIMD还需要注意些什么呢?

What’s SIMD

SIMD(Single Instruction Multiple Data) 是一种并行计算技术,可以同时对多个数据执行相同的操作。使用 SIMD 的主要目的是为了提升计算性能。

目前在大多数现代主流ARM、x64处理器上都支持SIMD,最初Pentium支持SIMD是为了更好地对多媒体(声音)进行处理,现代处理器增加了位宽更大的寄存器(128-bit、256-bit、512-bit),也增加了一些高效的指令。

老的x64(Intel、AMD)平台可以用SSE2…SSE4.2(128-bit),主流的x64(Intel、AMD)可以用AVX、AVX2(256-bit),最新的x64(Intel)可以用AVX-512(512-bit),其他平台可以自行检索下。

ps: 并行处理按照发生的粒度,可以划分为:任务并行(多核),指令并行(超标量流水线),数据并行(simd、vector、gpu)。

SIMD适用场景

适合使用 SIMD 的情况包括:

  • 需要对大批量数据执行相同的数学运算或逻辑运算,如向量、矩阵运算、图像处理等。
  • 需要对多媒体数据如音频、视频等进行处理,如编码、解码、滤波、变换等。
  • 在数据库、科学计算、金融分析等需要处理大量数值计算的场景。
  • 游戏开发中的物理模拟、人工智能等也可以使用 SIMD。

使用 SIMD 的好处有:

  • 提高计算并行度,单次指令处理更多数据。
  • 减少指令数,降低指令调度开销。
  • 更高效利用处理器内部执行单元。
  • 数据级并行,更易映射到多核架构。

一些常见使用 SIMD 的例子:

  • 图像处理:模糊、锐化、色彩空间转换等算法可以用SIMD加速。
  • 信号处理:FFT、FIR/IIR 滤波等用SIMD实现。
  • 科学计算:向量矩阵运算都可以用 SIMD 优化。
  • 数据压缩/解压:如音频视频编解码中的 SIMD 优化。
  • 数据库操作:聚集函数、关系运算可用 SIMD 实现。
  • 机器学习:神经网络中矩阵乘法、激活函数计算等使用 SIMD。

总之,SIMD 非常适合数据并行的场景,使用它可以显著提升计算性能。编译器和开发者都可以通过自动向量化和手动优化,利用 SIMD 使程序运行更快。

SIMD新手入门

这里以对两个数组进行求和为例,如果使用C来进行编码的话,基本逻辑应该是这样:

int nums1[LENGTH] = {1, 2, 3, 4, 5, 6, 7, 8};
int nums2[LENGTH] = {1, 1, 1, 1, 1, 1, 1, 1};
int result[LENGTH] = {0};

for (int i=0; i<LENGTH; i++) {
    result[i] = nums1[i] + nums2[i];
}

现在,我们考虑使用SSE、AVX2分别对其进行处理。

./add_2arrays_sse128.c:用SSE添加两个数组

#include <xmmintrin.h> // Need this in order to be able to use the SSE "intrinsics" (which provide access to instructions without writing assembly)
#include <stdio.h>

int main(int argc, char **argv) {
    float a[4], b[4], result[4]; // a and b: input, result: output
    __m128 va, vb, vresult; // these vars will "point" to SIMD registers

    // initialize arrays (just {0,1,2,3})
    for (int i = 0; i < 4; i++) {
        a[i] = (float)i;
        b[i] = (float)i;
    }
    
    // load arrays into SIMD registers
    va = _mm_loadu_ps(a); // https://software.intel.com/en-us/node/524260
    vb = _mm_loadu_ps(b); // same

    // add them together
    vresult = _mm_add_ps(va, vb);

    // store contents of SIMD register into memory
    _mm_storeu_ps(result, vresult); // https://software.intel.com/en-us/node/524262

    // print out result
    for (int i = 0; i < 4; i++) {
        printf("%f\n", result[i]);
    }
}

./add_2arrays_avx256.c:用AVX2添加两个数组:

在使用AVX2指令之前,我们必须对齐数组,否则会发生’段错误'。而且必须提供编译选项'-mavx2'。 AVX2支持在更先进和更新的CPU上,所以AVX2在gcc中默认是不启用的。

#include <immintrin.h> // Need this in order to be able to use the AVX "intrinsics" (which provide access to instructions without writing assembly)
#include <stdio.h>

int main(int argc, char **argv) {
    float a[8] __attribute__ ((aligned (32))); // Intel documentation states that we need 32-byte alignment to use _mm256_load_ps/_mm256_store_ps
    float b[8]  __attribute__ ((aligned (32))); // GCC's syntax makes this look harder than it is: https://gcc.gnu.org/onlinedocs/gcc-6.4.0/gcc/Common-Variable-Attributes.html#Common-Variable-Attributes
    float result[8]  __attribute__ ((aligned (32)));
    __m256 va, vb, vresult; // __m256 is a 256-bit datatype, so it can hold 8 32-bit floats

    // initialize arrays (just {0,1,2,3,4,5,6,7})
    for (int i = 0; i < 8; i++) {
        a[i] = (float)i;
        b[i] = (float)i;
    }

    // load arrays into SIMD registers
    va = _mm256_load_ps(a); // https://software.intel.com/en-us/node/694474
    vb = _mm256_load_ps(b); // same

    // add them together
    vresult = _mm256_add_ps(va, vb); // https://software.intel.com/en-us/node/523406

    // store contents of SIMD register into memory
    _mm256_store_ps(result, vresult); // https://software.intel.com/en-us/node/694665

    // print out result
    for (int i = 0; i < 8; i++) {
        printf("%f\n", result[i]);
    }
    
    return 0;
}

你可以gcc编译后尝试运行一下,但是为了更好地比较出这几种方式的性能差异,我们还是需要多执行几次,我们来写个benchmark测试。

./bench_add_2arrays.c,(不使用SIMD)将两个数组计算向量和1000w次

#include <stdio.h>

#define TIMES 10000000
#define LENGTH 8

int main(int argc, char *argv[])
{
    for (int k=0; k<TIMES; k++) {
        int nums1[LENGTH] = {1, 2, 3, 4, 5, 6, 7, 8};
        int nums2[LENGTH] = {1, 1, 1, 1, 1, 1, 1, 1};
        int result[LENGTH] = {0};
    
        for (int i=0; i<LENGTH; i++) {
            result[i] = nums1[i] + nums2[i];
        }
    }

    return 0;
}

./bench_add_2arrays_avx2.c,使用AVX2将两个数组计算向量和1000w次

#include <stdio.h>
#include <immintrin.h>

#define TIMES 10000000
#define LENGTH 8

#define i256 __m256i
#define avx256_set32 _mm256_set_epi32
#define avx256_add _mm256_add_epi32

int main(int argc, char *argv[])
{
    for (int k=0; k<TIMES; k++) {
        i256 first = avx256_set32(1, 2, 3, 4, 5, 6, 7, 8);
        i256 second = avx256_set32(1, 1, 1, 1, 1, 1, 1, 1);
        i256 result = avx256_add(first ,second);
        
        /*
        int *value = (int*)&result;
        for (int i=0; i<LENGTH; i++) {
            printf("%d ", value[i]);
        }
        printf("\n");
        */
    }
    return 0;
}

实际测试结果表明,AVX2的运行速度是普通方法的2倍:

$ time ./bench_add_2arrays

real    0m0.092s
user    0m0.092s
sys     0m0.000s
$ 
$ time ./bench_add_2arrays_avx2 

real    0m0.036s
user    0m0.036s
sys     0m0.000s

如果应用程序中类似操作很多,通过SIMD进行加速会获得明显的性能提升。

阅读更多

本文简单介绍了SIMD是什么,以及它有哪些应用场景,然后给出了一个求向量和问题中SIMD的应用示例,让读者直观感受了下SIMD的效率优势。但是并非所有计算场景都可以很容易联想到能通过simd来优化,需要巧妙地设计才能应用simd,比如simdjson中通过巧妙地查询表设计对字符进行分类、识别字符串位置等等。

本文讲的内容实际上非常浅显,读者可以搜索更多的simd应用场景来加深对simd的认识。

参考文献

  1. Practical SIMD Programming, http://www.cs.uu.nl/docs/vakken/magr/2017-2018/files/SIMD%20Tutorial.pdf

  2. C SIMD AVX2 Example, https://github.com/jean553/c-simd-avx2-example

  3. Intel® Intrinsics Guide, https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#expand=91,555&techs=AVX2

  4. Crunching Numbers with AVX and AVX2, https://www.codeproject.com/Articles/874396/Crunching-Numbers-with-AVX-and-AVX

    This article describes the datatypes and naming conventions, and how different intrinsics functions works.

  5. http://ftp.cvut.cz/kernel/people/geoff/cell/ps3-linux-docs/CellProgrammingTutorial/BasicsOfSIMDProgramming.html

    This article is also very helpful.