SIMD Intel Intrinsic 介绍

Posted by 武龙飞 on March 7, 2021

前言

上篇文章SIMD介绍说了SIMD的历史,以及SIMD汇编指令和寄存器,此篇文章讲述如何使用SIMD。使用SIMD有以下四个方案:

  • 使用库
  • 使用Intrinsic
  • 使用汇编
  • 使用Intel 的 ispc

出于学习的目的,这里使用Intrinsic。

Intrinsic 介绍

Intrisic是Intel使用汇编编写的C函数,在编译时会展开为内联(inline)函数,所以没有调用函数的开销。使用起来比较像在C语言中使用汇编。支持基本的操作,比如add,mul,div...,也有自己的数学相关函数,比如log,sin...。Intel有个网址专门来查询相关函数。

数据类型由三部分组成,都是以__m开头,然后是长度(128,256,512),最后是每条路径上的数据类型,默认空为float,其它为d/i分别为double和int。以下是基本数据类型举例,以及数据在内存中的排列。

__m128 f;           // {float f3, f2, f1, f0}
__m128d d;          // {double d1, d0}
__m128i i;          // {int i3, i2, i1, i0}
__m256  f;          // {float f7, ..., f1, f0}
__m512  f;          // {float f15, ..., f1, f0}

Intrinsic指令分为两种,Native指令是指直接和汇编指令是一对一的。Multi指令相当于一个函数,集成了几条指令,通过查看这些指令的类型,帮助我们衡量运行效率。例子如下:

  • Native instructions: _mm_add_ps(), _mm256_mul_pd(), …
  • Multi-instruction: _mm_setr_ps(), _mm512_set1_pd(), ..

Intrinsic还内置一些宏,来帮助我们更加简化程序的复杂性,也是分两类,一类为块操作,一类为简化程序的宏。例子如下:

  • Block operations: _MM_TRANSPOSE4_PS(), …
  • Helper: _MM_GET_EXCEPTION_MASK()…

Intrinsic 指令介绍

Intrinsic指令由三部分构成:_mmxxx_<op>_<suffix>,开头是以_mmxxx开头,其中xxx是长度,默认128时,xxx长度忽略,所以是_mm开头。然后是op操作指令,最后是后缀,比如:

float f[4] = {1.0f, 2.0f, 3.0f, 4.0f};
// 寄存器中内容:|1.0f|2.0f|3.0f|4.0f|
__m128 m = _mm_load_ps(f);
// 等价于
__m128 m = _mm_set_ps(4.0f, 3.0f, 2.0f, 1.0f);

其中ppackedssingle precision。和p向对应的是s表示scalar, 和s对应的还有d

Load and Store

将内存中的值,或立即数赋值给操作变量(operand),有两种方式: 第一种通过load指令

// p 16byte -aligned
__m128 a = _mm_load_ps(p);

// p not aligned
__m128 a = _mm_loadu_ps(P);

第二种set指令:

// 需要注意的是这些指令为组合指令
__m128 a = _mm_set_ps(1.0f, 2.0f, 3.0f, 4.0f);

将操作变量赋值给内存,使用store指令:

// p 类型为:float* mem_addr,内存地址必须是16byte对齐
// a 类型为:__m128
_mm_store_ps(p, a);

Constants

常量赋值使用set指令担当:

// lsb
// a = {1.0f, 2.0f, 3.0f, 4.0f}
__m128 a = _mm_set_ps(4.0f, 3.0f, 2.0f, 1.0f);

// b = {1.0f, 1.0f, 1.0f, 1.0f}
__m128 b = _mm_set1_ps(1.0);

// c = {1.0f, 0.0f, 0.0f, 0.0f}
__m128 c = _mm_set_ss(1.0f);

// d = {0.0f, 0.0f, 0.0f, 0.0f}
__m128 d = _mm_setzero_ps();

Arithmetic

加法运算__m128 c = _mm_add_ps(a, b);的运算如图所示:

a->|1.0    |2.0    |3.0    |4.0    |
                +
b->|0.2    |0.4    |0.6    |0.8    |
                ||
c->|1.2    |2.4    |3.6    |4.8    |

常见的数组相加:

void arr_add(float* a, float* b, int n)
{
    for(int i = 0;i < n; ++i)
    {
        a[i] += b[i];
    }
}

// n是4的倍数,a和b长度相等且都是16byte(4个float)对齐
void v_arr_add(float* a, float* b, int n)
{
    for(int i = 0; i < n; i += 4)
    {
        __m128 v1 = _mm_load_ps(a + i);
        __m128 v2 = _mm_load_ps(b + i);
        __m128 v = _mm_add_ps(v1, v2); 
        _mm_store_ps(a + i, v);
    }
}

Comparison

对比运算__m128 c = _mm_cmpeq_ps(a, b);的运算如图所示:

a->|1.0         |2.0            |3.0            |4.0            |
                                == 
b->|1.0         |0.4            |3.0            |0.8            |
                                ||
c->|0xffffffff  |0x0            |0xffffffff     |0x0            |

如果对比结果为true运算结果为0xffffffff,如果运算结果为false则运算结果为0x0

来看一个例子:

void saturate_arr(float* a, int n)
{
    for(int i = 0; i < n; ++i)
    {
        if(a[i] > 1)
        {
            a[i] = 1;
        }
        else if(a[i] < 0)
        {
            a[i] = 0;
        }
    }
}

void v_saturate_arr(float* a, int n)
{
    __m128 vmax = _mm_set1_ps(1.0f);
    __m128 vmin = _mm_set1_ps(0.0f);
    for(int i = 0; i < n; i+=4)
    {
        __m128 va = _mm_load_ps(a + i);                 // -0.2f, 1.4f      , 0.2f, 0.8f

        __m128 hmask = _mm_cmpgt_ps(va, vmax);          // 0    , 0xffffffff, 0   , 0
        __m128 hva = _mm_and_ps(hmask, va);             // 0    , 1.4f      , 0   , 0
        __m128 lva_left = _mm_sub_ps(va, hva);          // -0.2f, 0         , 0.2f, 0.8f 
        __m128 hone = _mm_and_ps(hmask, vmax);          // 0    , 1.0f      , 0   , 0
        __m128 hva_one = _mm_add_ps(lva_left, hone);    // -0.2f, 1.0f      , 0.2f, 0.8f

        __m128 lmask = _mm_cmplt_ps(va, vmin);          // 0xfffffff, 0    , 0   , 0
        __m128 lva = _mm_and_ps(va, lmask);             // -0.2f    , 0    , 0   , 0 
        __m128 hva_left = _mm_sub_ps(hva_one, lva);     // 0        , 1.0f , 0.2f, 0.8f

        _mm_store_ps(a + i, hva_left);
    }

Shuffles

首先来看取ab的三位和四位组合成新的变量c_mm_unpackhi_ps(__m128 a, __m128 b)表示如下:

// a 和 b为LSB,从左至右索引依次为 0,1,2,3
a->|1.0         |3.0            |5.0            |7.0            |
                                
b->|2.0         |4.0            |6.0            |8.0            |
                                ||
c->|5.0         |6.0            |7.0            |8.0            |

_mm_unpacklo_ps(__m128 a, __m128 b)表示如下:

// a 和 b为LSB,从左至右索引依次为 0,1,2,3
a->|1.0         |3.0            |5.0            |7.0            |
                                
b->|2.0         |4.0            |6.0            |8.0            |
                                ||
c->|1.0         |2.0            |3.0            |4.0            |

__m128 _mm_shuffle_ps (__m128 a, __m128 b, unsigned int imm8)表示如下:

// a 和 b为LSB,从左至右索引依次为 0,1,2,3
// __m128有4路浮点值,二进制2位(00-11),可以标识4路
// imm8 前8位,每两位取值得到一个索引(index),每个索引取值如下:
// [1:0] 所得到的index去a中选取值
// [3:2] 所得到的index去a中选取值
// [5:4] 所得到的index去b中选取值
// [7:6] 所得到的index去b中选取值
// imm8的值假设为:0x01 11 00 10
a->|1.0         |3.0            |5.0            |7.0            |
                                
b->|2.0         |4.0            |6.0            |8.0            |
                                ||
c->|5.0         |1.0            |8.0            |4.0            |
float sum_simd(const float* a, size_t n)
{
    __m128 sRet = _mm_set1_ps(0);
    for(size_t i = 0; i < n; i+=4)
    {
        __m128 m = _mm_load_ps(&a[i]);
        sRet = _mm_add_ps(sRet, m);
    }
                                                                // 1, 2, 3, 4
    __m128 tmp = _mm_unpackhi_pd(sRet, sRet);                   // 3, 4, 3, 4
    tmp = _mm_add_ps(tmp, sRet);                                // 4, 6, 6, 8
    sRet = _mm_shuffle_ps(tmp, tmp, _MM_SHUFFLE(0, 0, 0, 1));   // 6, 4, 4, 4 -> 00, 00, 00, 01
    sRet = _mm_add_ps(tmp, sRet);                               // 10, 12, 10, 14

    return *(float*)&sRet;
}

总结

本篇文章介绍了Intel的Intrinsic数据类型,以及常用函数。通过这些基本指令的学习,可以帮助后续实践过程中更深的认知。