暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

并发编程:SIMD 介绍

coredump 2021-10-01
1924

SIMD 简介

众所周知,计算机程序需要编译成指令才能让 CPU 识别并执行运算。所以,CPU 指令处理数据的能力是衡量 CPU 性能的重要指标。

为了提高 CPU 指令处理数据的能力,半导体厂商在 CPU 中推出了一些可以同时并行处理多个数据的指令 —— SIMD[1] 指令。

SIMD 的全称是 Single Instruction Multiple Data,中文名“单指令多数据”。顾名思义,一条指令处理多个数据。

如上图所示:

  • 一个普通加法指令,一次只能对两个数执行一个加法操作。
  • 一个 SIMD 加法指令,一次可以对两个数组(向量)执行加法操作。

SIMD 简史

经过多年的发展,支持 SIMD 的指令集有很多。各种 CPU 架构都提供各自的 SIMD 指令集,本文的介绍以 x86 架构为主。

1997 年,Intel 推出了第一个 SIMD 指令集 —— MultiMedia eXtensions(MMX)。MMX 指令主要使用的寄存器为 MM0 ~ MM7,大小为 64 位。

1999 年,Intel 在 Pentium III 对 SIMD 做了扩展,名为 Streaming SIMD eXtensions(SSE)。SSE 采用了独立的寄存器组 XMM0 ~ XMM7,64位模式下为 XMM0 ~ XMM15 ,并且这些寄存器的长度也增加到了 128 位。

2000 年,Intel 从 Pentium 4  开始引入 SSE2。

2004年,Intel 在 Pentium 4 Prescott 将 SIMD 指令集扩展到了 SSE3。

2006 年,Intel 发布 SSE4 指令集,并在 2007 年推出的 CPU 上实现。

2008 年,Intel 和 AMD 提出了 Advanced Vector eXtentions(AVX)。并于 2011 年分别在 Sandy Bridge 以及 Bulldozer 架构上提供支持。AVX 对 XMM 寄存器做了扩展,从原来的128 位扩展到了256 位。

2013年,Intel 在发布的 Haswell 处理器上开始支持AVX2。同年,Intel 提出了 AVX-512。

2016 年,Xeon Phi x200 (Knights Landing) 是第一款支持了 AVX-512 的 CPU。如扩展名所示,AVX-512 主要改进是把 SIMD 寄存器扩展到了 512 位。

如何使用 SIMD?

  1. 编译器自动向量化。

一些比较简单的场景,编译器可以自动将目标代码向量化(auto vectorization)。

使用 GCC 可以通过 -S 参数,输出中间汇编文件,以检查是否自动将代码进行向量化了:

  • 以 v 开头的指令,如 vpmulld、vpaddd、vmovdqu 都是向量化指令。
  • xmm、ymm、zmm 分别表示 128 位、256 位 和 512 位的向量化使用的寄存器。
  • 本例中使用了 -mavx512f 编译选项要求编译器使用 AVX512,所以汇编代码使用的寄存器是 512 位的 zmm 系列。
  • 本例中将数组 x 的长度固定为 64 个 int(512 位的倍数),是为了让实例生成的汇编代码更加简洁,不是向量化的强制要求。
  1. 编译器扩展的向量支持[2]

让编译器进行自动向量化,其实有点“看运气”的成分。你可以使用编译器扩展的向量支持能力来实现向量化。如:

#include <iostream>
#include <string.h>
#include <string>

// v16si 表示 16 个 int 的向量(数组),长度为 64 字节
typedef int v16si __attribute__ ((vector_size (64)));

std::string toString(const v16si & v) {
    std::string s = "[ ";
    for (int i = 0; i < 16; i++) {
        s += std::to_string(v[i]) + " ";
    }
    s += "]";
    return s;
}

int main() {
    v16si v0, v1;
    memset(&v0, 0sizeof(v0));
    memset(&v1, 0sizeof(v1));
    std::cout << "v0: " << toString(v0) << std::endl;
    std::cout << "v1: " << toString(v1) << std::endl;

    v0 = v0 + 1;  // v0 + {1, 1, ..., 1}
    std::cout << "v0 = v0 + 1: " << toString(v0) << std::endl;

    v1 = v1 + 2;  // v1 + {2, 3, ..., 2}
    std::cout << "v1 = v1 + 2: " << toString(v1) << std::endl;

    v0 = v0 + v1;
    std::cout << "v0 = v0 + v1: " << toString(v0) << std::endl;

    v1 = v0 * v1;
    std::cout << "v1 = v0 * v1: " << toString(v1) << std::endl;

    v16si a = {0123456789101112131415};
    v16si b = {0246813579101112131415};
    // Vectors are compared element-wise producing 0 when comparison is false 
    // and -1 (constant of the appropriate type where all bits are set) otherwise. 
    auto c = a > b;
    std::cout << "c = a > b: " << toString(c) << std::endl;

    auto d = (a > b) ? v0 : v1;
    std::cout << "d = (a > b) ? v0 : v1: " << toString(d) << std::endl;

    return 0;
}

编译:g++ -o built-vec -mavx512f built-vec.cc

-mavx512f 表示使用 AVX512 指令集。如果 CPU 不支持 AVX512,可以使用其它 SIMD 指令集。相关编译选项有:-mmmx、-msse、-msse2、-msse3、-mssse3、-msse4.1、-msse4.2、-msse4、-mavx、-mavx2、-mavx512f、-mavx512pf、-mavx512er、-mavx512cd、-mavx512vl、-mavx512bw、-mavx512dq、-mavx512ifma、-mavx512vbmi。

  1. 指令封装

参考 The Intel Intrinsics Guide[3] 这里,主流的几个编译器 gcc、clang 和 msvc 都将 SIMD 指令封装成 C 函数,方便使用,具体可以参考官方文档。

参考资料

[1]

SIMD: https://en.wikipedia.org/wiki/SIMD

[2]

扩展的向量支持: https://gcc.gnu.org/onlinedocs/gcc/Vector-Extensions.html

[3]

The Intel Intrinsics Guide: https://software.intel.com/sites/landingpage/IntrinsicsGuide/


文章转载自coredump,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论