SIMD的编写

前言

  学习SIMD的笔记

参考教程:SIMD Tutorial.pdf

一、使用SIMD的场景

  考虑如下代码:

vec3 velocity = GetPlayerSpeed(); 
float length = velocity.Length(); 

  获取玩家速度,是个包含x,y,z三个分量的向量,然后对这个向量求长度。
这个Length让我们写的话,一般就是:

float Length(){
    return sqrt(x*x+y*y+z*z);
}

  或者:

x = velocity.x * velocity.x;
y = velocity.y * velocity.y;
z = velocity.z * velocity.z;
sum = x + y;
sum = sum + z;
length = sqrtf( sum ) 

  此时对前面三个值求平方就可以是并行的,但这同时是一种浪费。

  CPU支持的指令集可以通过CPU-Z来查看:
可以看出,我的CPU同时支持SSE和AVX指令集,其中SSE支持4字(16字节,128位)运算,AVX支持8字(32字节,256位)运算。

  三个并行的浮点运算只会占用其中的一部分插槽,这样会浪费25%的SSE数据槽位或62.5%的AVX数据槽位。
  但是如果我们用另一种方式;考虑我们如果有四个玩家,那么代码会是如下的方式组织:

x4 = GetPlayerXSpeeds();
y4 = GetPlayerYSpeeds();
z4 = GetPlayerZSpeeds();
x4squared = x4 * x4;
y4squared = y4 * y4;
z4squared = z4 * z4;
sum4 = x4squared + y4squared;
sum4 = sum4 + z4squared;
length4 = sqrtf4( sum4 );

  x4,y4,z4都是承载着四个浮点数的向量,一次取出四个玩家的同一分量,同时对这一分量做运算,从新的维度操作数据,使指令集的利用率得到提高,从这点上看,一次操作的数据数量最好是4或8的倍数。
  同时也可以看出,为了更好的SIMD使用,最好配合高效的数据结构。

二、数据操控

  C++是强类型语言,但对于一个数据,我们可以把它看做另一个类型来操控它:

int a;
float& b = (float&)a;

  也可以用联合体的方式来操控:

union { int a; float b; }

  假如我们用如下方式写出联合体,那么我们既可以当成无符号整形一次性运算或赋值,也可以当成无符号字符类型依次取值或运算:

union { unsigned int c4; unsigned char c[4]; }; 

实验代码:

union int_char { int c4; char c[4]; };
int main(){
    int_char ic;
    ic.c[0] = 0; ic.c[1] = 0;
    ic.c[2] = 0; ic.c[3] = 1;
    std::cout << ic.c4 << std::endl;
// output:
//  16777216 = 1 * (16^6)
}

  由此可见int的存储形式(英特尔CPU在PC上是小端处理方式)。

SIMD数据类型

  首先引入两个头文件:

#include "nmmintrin.h" // for SSE4.2
#include "immintrin.h" // for AVX

  其中__m128和__128i是SSE的数据类型,代表四位浮点数和四位整数,__m256和__256i则是AVX的。
  __m128包含四位浮点数,因此我们同样可以使用上面的技巧:

union float4{ __m128 f4; float f[4]; };

  这种用法称为类型双关,不过我们按ctrl点入__m128中,会发现这本身就是一个多关类型:

typedef union __declspec(intrin_type) __declspec(align(16)) __m128 {
     float               m128_f32[4];
     unsigned __int64    m128_u64[2];
     __int8              m128_i8[16];
     __int16             m128_i16[8];
     __int32             m128_i32[4];
     __int64             m128_i64[2];
     unsigned __int8     m128_u8[16];
     unsigned __int16    m128_u16[8];
     unsigned __int32    m128_u32[4];
 } __m128;

  我们甚至可以直接:__m128 f4 = {1,2,3,4};,这样会存储为float类型,用f4.m128_f32可以查看,用f4.m128_i32则会发生值的错误;存储顺序会按照数组的书写顺序存储。
  用以下的方法塞入数字:

float4 a, b;
a.f4 = _mm_set_ps(4.0f, 4.1f, 4.2f, 4.3f);
b.f4 = _mm_set_ps(1.0f, 1.0f, 1.0f, 1.0f);

  可以塞入不同类型的数值,上例子中塞入的是float值,和数组式初始化不同的是,它的存储顺序刚好相反,类似最上边的联合体存取方式,是小端存储。
  然后调用方法使它们并行相加:

__m128 sum4 = _mm_add_ps(a.f4, b.f4);

  此时我们想办法将其打印出来:

float4 sum;
sum.f4 = sum4;
std::ostream_iterator foi = { std::cout, " " };//STL中的输出迭代器
std::copy(std::begin(sum.f), std::end(sum.f), foi);//输出
//output:
//5.3 5.2 5.1 5

  同样的运算操作,还有减、乘、除、开方、取倒数:

_mm_sub_ps(a, b);
_mm_mul_ps(a, b);
_mm_div_ps(a, b);
_mm_sqrt_ps(a, b);
_mm_rcp_ps(a, b);// reciprocal

  对于AVX指令集的操作类似,不过运算函数的名称都要在"_mm"后加上256,例如:“_mm256_add_ps”

你可能感兴趣的:(SIMD的编写)