simd 本身是 CPU 的一部分,不是 GPU 的一部分。

SIMD(Single Instruction Multiple Data) 即单指令流多数据流,是一种采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术。简单来说就是一个指令能够同时处理多个数据。

simd 指令包含了:

  • MMX(MultiMedia eXtension) 拓展指令集。可以使用 64bit 的 MM0-MM7 八个寄存器。

  • SSE(Streaming SIMD Extensions) 拓展指令集。可以使用 128bit XMM0-XMM7 八个寄存器。后续又添加了 8 个寄存器 XMM8-XMM15。

  • AVX 将 SSE 的 128bit 寄存器拓展到了 256bit,称为 YMM。

  • AVX512 将 AVX 的 256bit 寄存器拓展到了 512 bit,称为 ZMM 寄存器。

AVX512 指令会导致 CPU 性能下降,从而导致效果反而不如 AVX,因此 Rust 编译时会避开 avx512 指令集。

使用 SIMD 的方法

使用 SIMD 的方法从简单到困难如下所示:

  1. 依赖编译器做优化。

  2. 使用 Intel 提供的 IPP 函数库。

  3. 使用 OpenMP 中的指令 #pragma omp simd

  4. 使用 intrinsics 函数。

  5. 使用汇编指令。

intrinsics

intrinsics 相关的函数 [1] 包含在了下面的头文件中,其中下面的头文件包含上面的头文件,因此只需要包含最后一个即可:

头文件能力

xmmintrin.h

SSE, 支持同时对4个32位单精度浮点数的操作。

emmintrin.h

SSE 2, 支持同时对2个64位双精度浮点数的操作。

pmmintrin.h

SSE 3, 支持对SIMD寄存器的水平操作(horizontal operation),如hadd, hsub等…​。

tmmintrin.h

SSSE 3, 增加了额外的instructions。

smmintrin.h

SSE 4.1, 支持点乘以及更多的整形操作。

nmmintrin.h

SSE 4.2, 增加了额外的instructions。

immintrin.h

AVX, 支持同时操作8个单精度浮点数或4个双精度浮点数。

这些头文件提供的函数命名规则为:

__<return_type> _<vector_size>_<intrin_op>_<suffix>
  • return_type 统一为各种类型的向量类型。

  • vector_size 位 \_128, \_256, \_512 等,代表了寄存器的大小。

  • intrin_op 代表了函数执行的操作。目前存在的操作有:[2]

    操作作用

    load

    将数据储存到寄存器中。

    store

    将寄存器中的值写回到内存中。

  • suffix 后缀可能为:

    后缀作用

    p

    packed

    s

    float

    d

    double

下面是一个使用 SIMD 寄存器加速拷贝的例子。对于 1024B 的 u8 array 而言,循环拷贝每次拷贝 1B,需要拷贝 1024 次,的使用 ZMM 寄存器可以每次拷贝 512/8=64B,只需要拷贝 16 次。

#include <chrono>
#include <cstring>
#include <iostream>
#include <string>

extern "C" {
#include <immintrin.h>
}

using namespace std::chrono;
using namespace std::chrono_literals;

#define EXPAND(i)                                     \
    {                                                 \
        auto a = _mm512_loadu_si512(data + (i) * 64); \
        _mm512_storeu_si512(dst + (i) * 64, a);       \
    }
int main(int argc, char *argv[]) {
    auto start = steady_clock::now();

    auto data = new uint8_t[1024];
    for (int i = 0; i < 1024 / 64; ++i) {
        auto ptr = (uint64_t *)data;
        *(ptr + i) = i;
    }
    auto dst = new uint8_t[1024];

    uintptr_t send = 0;
    while (true) {
        // for (int i = 0; i < 1024; ++i) { (1)
        //     dst[i] = data[i];
        // }
        for (int i = 0; i < 16; i += 2) { (2)
            auto a = _mm512_loadu_si512(data + i * 64);
            _mm512_storeu_si512(dst + i * 64, a);
        }
        // memcpy(dst, data, 1024); (3)

        send += 1;
        auto end = steady_clock::now();
        auto duration = duration_cast<seconds>(end - start);
        if (duration.count() >= 1) {
            std::cout << send * 1.f / 1024 / 1024 << "GB" << std::endl;
            send = 0;
            start = end;
        }
    }

    return 0;
}
  1. 使用 for 循环拷贝。

  2. 使用 simd 指令拷贝。

  3. 使用 memcpy 拷贝。

在 x86 计算机 debug 模式编译的情况下。for 循环 1GB/s,simd 15 GB/s,memcpy 34GB/s。

其中对于 simd:

  • 使用循环完全展开后增长到了 20GB/s。

  • 除去临时变量 a 后速度为 25GB/s。

  • 将上述两个优化连在一起后速度并没有什么改善。

Last moify: 2022-12-04 15:11:33
Build time:2025-07-18 09:41:42
Powered By asphinx