SIMD以及llvm优化的一些理解(持续更新...)

822 阅读3分钟

SIMD

关于SIMD的定义,wiki给出的解释是:

Single instruction, multiple data (SIMD) is a type of parallel processing in Flynn's taxonomy. SIMD can be internal (part of the hardware design) and it can be directly accessible through an instruction set architecture (ISA), but it should not be confused with an ISA. SIMD describes computers with multiple processing elements that perform the same operation on multiple data points simultaneously.

简单来说,SIMD就是硬件提供的"一条指令操作多条数据"的方式。一般而言,一份c++代码可以在3个层面使用SIMD优化手段:

  1. 例如clang, gcc, msvc编译器在开启代码优化之后,会根据代码的情况选择是否使用SIMD指令;
  2. 在c++中直接使用SIMD intrinsics指令,在X86-64 windwos中使用SIMD intrinsics,需要#include <emmintrin.h>等头文件; 在arm linux/android中使用SIMD intrinsics,需要#include<arm_neon.h>头文件;
  3. 直接在汇编层面使用SIMD汇编指令;
  • 第一部分是编译器的行为,具体会在LLVM的优化一节中写出;
  • 第二部分是可以由c++程序员自己控制的行为,也是绝大多数人进行针对性SIMD优化的层面;
  • 第三部分直接写对应平台的汇编,这一部分要求程序员非常熟悉对应平台的指令集,系统以及汇编语言,难度相当大。

ARM Neon

arm Neon Intrinsics

X86/64 SSE and AVX

x86 and amd64 instruction reference

LLVM的优化 Auto-Vectorization

这篇Auto-Vectorization in LLVM详细介绍了llvm是如何做 Auto-Vectorization的优化的,简单来说分为如下几个情况。

Loops with unknown trip count

void bar(float *A, float* B, float K, int start, int end) {
  for (int i = start; i < end; ++i)
    A[i] *= B[i] + K;
}

对于这种情况,因为startend都不确定,编译器甚至不清楚这个for循环会不会进入,因此这种情况编译器不会堆for循环进行Vectorization。

Runtime Checks of Pointers

void bar(float *A, float* B, float K, int n) {
  for (int i = 0; i < n; ++i)
    A[i] *= B[i] + K;
}

这种情况在Memory Aliasing一文中提到过,因为编译器不知道A,B指针是否指向同一块内存,所以一般不采取Vectorization,但当程序员明确告诉编译器,例如加上restrict关键词,编译器就会根据实际情况采用Vectorization。

Reductions

int foo(int *A, int n) {
  unsigned sum = 0;
  for (int i = 0; i < n; ++i)
    sum += A[i] + 5;
  return sum;
}

此处编译器做了一个优化,因为sum的类型和数组A的类型并不匹配,因此llvm vectorizersum转成一个int数组,这样在循环里面其实就可以进行Vectorization优化,在循环结束后,编译器会将这个临时生成的int数组的结果,放到变量sum中。

Inductions

void bar(float *A, int n) {
  for (int i = 0; i < n; ++i)
    A[i] = i;
}

对于这种情况,编译器也会进行Vectorization。

If Conversion

int foo(int *A, int *B, int n) {
  unsigned sum = 0;
  for (int i = 0; i < n; ++i)
    if (A[i] > B[i])
      sum += A[i] + 5;
  return sum;
}

对于这种循环里面有if的情况,Loop Vectorizer会"flatten"if这种情况。简单来说,就是编译器在if的这种情况下做了更为复杂的判断和运算,以达到优化的效果。

Pointer Induction Variables

int baz(int *A, int n) {
  return std::accumulate(A, A + n, 0);
}

因为c++的迭代器本质上就是指针,因此对于这种情况编译器也能进行Vectorization。

Reverse Iterators

void foo(int *A, int n) {
  for (int i = n; i > 0; --i)
    A[i] +=1;
}

因为编译器知道这个loop何时会结束,因此这种情况也能进行Vectorization。

Scatter/Gather

void foo(int * A, int * B, int n) {
  for (intptr_t i = 0; i < n; ++i)
      A[i] += B[i * 4];
}

对于这种情况,因为i的类型是intptr_t,因此编译器将默认不会进行Vectorization,不过程序员可以通过添加编译参数-mllvm -force-vector-width=#来进行强制优化。

Vectorization of Mixed Types

void foo(int *A, char *B, int n) {
  for (int i = 0; i < n; ++i)
    A[i] += 4 * B[i];
}

这种情况因为涉及到了不同类型的混合运算,不同的类型的混合运算对于SIMD寄存器而言是不友好的,因此编译器会根据实际情况来判断优化是否"合算"再来优化。

Global Structures Alias Analysis

struct { int A[100], K, B[100]; } Foo;

void foo() {
  for (int i = 0; i < 100; ++i)
    Foo.A[i] = Foo.B[i] + 100;
}

这种情况下编译器会进行Vectorization。

Vectorization of function calls

void foo(float *f) {
  for (int i = 0; i != 1024; ++i)
    f[i] = floorf(f[i]);
}

此处的function calls指的是一些标准库的内置函数,例如:

image.png

编译器知道这些函数在这样的场景下可以转变为什么样的SIMD指令,因此此处也是会Vectorization。在llvm中,如果想得到更好的math library函数的优化效果,可以增加编译参数-fno-math-errno

Partial unrolling during vectorization

int foo(int *A, int n) {
  unsigned sum = 0;
  for (int i = 0; i < n; ++i)
      sum += A[i];
  return sum;
}

因为现代处理器一般都是多核,因此还有一种优化策略就是编译器将这个for展开,这样就可以有多核参与到这个运算中,加快了计算。

Reference

Intel® Intrinsics Guide

x86 Intrinsics Cheat Sheet

Compiler intrinsics

Is software prefetching (__builtin_prefetch) useful for performance?

【预取简介】[Prefetching Introduction]

x86 Instruction Set Reference PREFETCHh

CPU Caches