SIMD能力初体验,你了解多少?

商务办公
SIMD技术在大数据和机器学习领域有非常广泛的应用。Clickhouse为什么快,NumPy为什么快,背后都离不开SIMD技术的支持。那么SIMD到底是什么呢,我们来看看。

SIMD,Single Instruction Multiple Data,是一种在CPU指令级别支持的并行处理技术。大家最早听说这个词,应该是在《计算机组成原理》的课上。

为了体现出区别,我们先看最简单的模式:Single Instruction Single Data (SISD)。这种模式下,一个单核CPU接收并执行一条指令。该指令只加载内存单元里的一条数据到寄存器,然后进行处理。

Single Instruction Single Data

SIMD模式下,CPU的寄存器通常比较大,比如128bit,目前最新已支持到512bit。如果我们使用512bit寄存器,那么一次性就可以加载8个int64数字,以并行度=8的速度进行计算:

Single Instruction Multiple Data

当然,还有两个分类 MISD 和 MIMD,这里就不细说了。

Intel CPU对SIMD的支持

Intel CPU通过扩充指令集提供了对SIMD的支持。按照出现顺序,总共有三套:MMX、SSE 和 AVX:

我们可以通过Intel官方网站查询自己的处理器是否支持(地址附在文章末尾)。下面以MacOS为例,简单看一下。通过sysctl查看CPU型号:

sysctl -a | grep brand 
machdep.cpu.brand_string: Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz
machdep.cpu.brand: 0

下面是查询结果,可见主流的SSE和AVX指令集都是支持的:

那么这些指令集怎么用呢?Intel官方提供了一套C语言库,并且有详细的函数文档,名字为 "Intel® Intrinsics Guide"。

这些函数有明确的命名规范,由三段构成,分别是:

  1. _mm<位数>_mm 128bit,_mm256 256bit,_mm512 512bit。
  2. _<运算>_add 加, _sub 减,_mul 乘,_div 除,与或非同理。
  3. _<原始类型>_epi16 int16, _epi32 int32, _ps float32, _pd float64。

比如我想看下256bit下的加法,搜索 mm256_add 会返回一组函数:

接下来我们用这些指令来看看下性能吧。

准备工作

由于要做性能测试,编程语言是C/C++,所以选择 google/benchmark 作为辅助。测试场景是两个100w条数据的数组做加法,数组里的元素可以是int32、float32、int64等。后面我们采用float32进行测试。

google/benchmark 跟着Github上"Installation" 部分走就好了,最后必须执行安装这一步:

sudo cmake --build "build" --config Release --target install

编写代码:

先写一段比较正常的单测代码,通过 #include <immintrin.h>可使用SIMD的能力。准备工作包括:

  1. 初始化3个长度为100w的数组 a、b、c, _mm_malloc负责内存分配。
  2. 对 a 和 b 进行初始化。

计算逻辑是 c = a + b,跑多少轮次由 benchmark::State &state 来控制。代码如下:

#include <immintrin.h>
#include <benchmark/benchmark.h>

constexpr int N = 1000000;

static void normal(benchmark::State &state)
{
    float *a = static_cast<float *>(_mm_malloc(sizeof(float) * N, 16));
    float *b = static_cast<float *>(_mm_malloc(sizeof(float) * N, 16));
    float *c = static_cast<float *>(_mm_malloc(sizeof(float) * N, 16));
    for (int i = 0; i < N; ++i)
    {
        a[i] = i;
        b[i] = 2 * i;
    }

    for (auto _ : state)
    {
        for (int i = 0; i < N; ++i)
        {
            c[i] = a[i] + b[i];
        }
    }

    _mm_free(a);
    _mm_free(b);
    _mm_free(c);
}

BENCHMARK(normal);

我们将文件命名为 benchmark_float32.cpp。编译并执行:

g++ -Wall -std=c++20 -msse4 -mavx512f -mavx512bw benchmark_float32.cpp -pthread -lbenchmark -o benchmark_float32

由于需要支持sse4 avx512,编译时需要加上 -msse4 -maxv512f -mavx512bw。运行 ./benchmark_float32 结果如下:

2023-06-17T18:30:04+08:00
Running ./benchmark_float32
Run on (8 X 2300 MHz CPU s)
CPU Caches:
  L1 Data 48 KiB
  L1 Instruction 32 KiB
  L2 Unified 512 KiB (x4)
  L3 Unified 8192 KiB
Load Average: 3.24, 3.72, 4.09
-----------------------------------------------------
Benchmark           Time             CPU   Iterations
-----------------------------------------------------
normal        1821404 ns      1812256 ns          386

到当前为止,测试能够跑起来了。我们再加一个 128bit 计算的支持。这需要3个函数:

  1. _mm_load_ps 将4个打包的float32加载到一个__m128类型的变量里。
  2. _mm_add_ps 对2个 __m128类型的变量做加法。
  3. _mm_store_ps 将1个__m128类型的变量存到一个float32*指向的内存里。

组装起来就是:

for (int i = 0; i < N; i += 4)
{
    __m128 v1 = _mm_load_ps(a + i);
    __m128 v2 = _mm_load_ps(b + i);
    __m128 v3 = _mm_add_ps(v1, v2);
    _mm_store_ps(c + i, v3);
}

由于一个 __m128类型的变量可以容纳4个float32,所以 i 每次加4。

同样的方法,我们可以把 __m256 和 __m512 都纳入测试,测试结果如下:

可以发现,这些扩容指令集的执行性能还是不错的,不过由于load和store需要额外的时间,并没有倍数的提升。

同样的方式,我们拿 int32 和 int64 进行测试,测试结果如下:

我们看 Iterations 这个指标,这个指标越大,说明运行速度越快。我们将不同类型汇总成表格:

Iterations

float32

int32

int64

float32-O3

int64-O3

normal

388

383

361

1541

516

sse128

573

709

345

1441

2716

avx256

943

1176

550

1435

2959

avx512

1791

1872

759

6611

2601

可以发现,float32 和 int32 的迭代轮次逐个增加,而int64 在sse128和normal模式下基本持平,avx256和avx512呈现增加趋势。可能原因是在sse128模式下,计算速度理论上增加100%,load和save的时间抵消了计算速度的收益。

另外,在编译时开启 -O3 最高级别的编译优化之后, int64 在 SIMD下提速非常明显,float32只有在 avx512模式下有明显提速。

除了简单的加减乘除,SSE 和 AVX 能够支持很多形式的计算,包括但不限于:位运算、类型转换、比较、加密算法、数学函数、统计函数、随机数、字符串等,完整列表可参考 "Intel® Intrinsics Guide"。由于只是学习目的,并没有对所有这些函数进行测试。

问了下ChatGPT “SIMD的劣势”,给出的回复如下:

  • 需要特定的硬件支持:SIMD指令是由CPU硬件支持的,需要特定的硬件支持才能使用。如果CPU不支持SIMD指令集合,就无法使用SIMD指令。
  • 编码复杂度高:使用SIMD指令需要对指令集有深入的了解和熟练运用,编码过程相对比较复杂。
  • 数据结构限制:SIMD指令适用于处理顺序存储的数据,并且需要按照一定的方式将数据打包。因此,在某些情况下,需要进行数据结构的转换才能使用SIMD指令。
  • 无法处理分支:SIMD指令是在数据流上进行操作,不能直接处理分支,需要将分支转换为数据流并按照特定的方式处理分支。
  • 容易出现精度误差:使用SIMD指令进行高精度计算时,可能会出现精度误差。这是由于浮点数精度有限,并且处理过程中可能会将数据转换为其他精度的数据类型而导致的。

单纯从应用上来看,Clickhouse启发了一众大数据处理框架对SIMD能力的引入、NumPy库的引入,还有多媒体领域的广泛使用,我们有理由相信SIMD带来的性能收益。

Clickhouse具体做了哪些优化,对SIMD的能力的应用有多充分,后面了解完再补充。

查询CPU信息:
https://ark.intel.com/content/www/us/en/ark.html。

责任编辑:姜华 来源: 今日头条
相关推荐

2009-08-01 09:06:35

UbuntuOneLinux开源操作系统

2009-03-09 15:12:39

XenServer安装

2023-10-25 08:17:06

Lite模式代理类

2012-12-27 10:58:24

KVMKVM概念

2023-10-29 08:35:47

AndroidAOP编程

2021-06-06 18:22:04

PprofGopher逻辑

2020-03-25 08:47:22

智能边缘边缘计算网络

2020-11-06 15:12:21

人工智能技术无人驾驶

2010-11-22 10:31:17

Sencha touc

2011-05-30 15:12:10

App Invento 初体验

2015-11-09 10:44:37

DevOpsIT运维

2023-08-17 10:12:04

前端整洁架构

2023-12-24 12:56:36

协程

2021-12-09 07:47:58

Flink 提交模式

2020-12-10 09:00:00

开发.NET工具

2023-09-07 10:26:50

接口测试自动化测试

2022-02-08 12:06:12

云计算

2022-06-07 07:37:40

线程进程开发

2019-08-07 17:18:18

云计算云原生函数

2011-08-23 11:03:35

ATM
点赞
收藏

51CTO技术栈公众号