首页 > 其他分享 >SSE和AVX指令基本使用

SSE和AVX指令基本使用

时间:2024-08-26 23:51:06浏览次数:6  
标签:__ ps AVX mm 指令 m128i SSE SIZE

SSE/AVX 指令属于 Intrinsics 函数,由编译器在编译时直接在调用处插入代码,避免了函数调用的额外开销。但是与 inline 函数不同,Intrinsics 函数的代码由编译器提供,能够更高效地使用机器指令进行优化调整。一般的函数是在库中,Intrinsic Function 内嵌在编译器中(built in to the compiler)。

优化器(Optimizer)内置的一些 Intrinsic Function 行为信息,可以对 Intrinsic 进行一些不适用于内联汇编的优化,所以通常来说 Intrinsic Function 要比等效的内联汇编(inline assembly)代码更快。优化器能够根据不同的上下文环境对 Intrinsic Function 进行调整。例如,以不同的指令展开 Intrinsic Function,将 buffer 存放在合适的寄存器中。

关于 SSE 和 AVX 内部函数的相关信息可以在Intel® Intrinsics Guide查看。

1 包含头文件

SSE 和 AVX 指令集有多个不同的版本, 其对应的 Intrinsic 包含在不同的头文件中,如果确定只使用某个版本的 SSE/AVX 指令则只包含相应的头文件即可。

如果不关心具体版本则可以使用<intrin.h>包含所有版本的头文件内容。

#include <intrin.h> 

Intrinsic 头文件和 SIMD(SSE/AVX) 指令集,已经 Visual Studio 的版本对应关系

Instrinsic 头文件指令集描述Visual Studio版本号Visual Studio版本名
intrin.hAll Architectures8.02005
mmintrin.hMMX intrinsics6.06.0 SP5+PP5
xmmintrin.hStreaming SIMD Extensions intrinsics6.06.0 SP5+PP5
emmintrin.hWillamette New Instruction intrinsics (SSE2)6.06.0 SP5+PP5
pmmintrin.hSSE3 intrinsics9.02008
tmmintrin.hSSSE3 intrinsics9.02008
smmintrin.hSSE4.1 intrinsics9.02008
nmmintrin.hSSE4.2 intrinsics9.02008
wmmintrin.hAES and PCLMULQDQ intrinsics10.02010
immintrin.hIntel-specific intrinsics(AVX)10.02010 SP1
ammintrin.hAMD-specific intrinsics (FMA4, LWP, XOP)10.02010 SP1
mm3dnow.hAMD 3DNow! intrinsics6.06.0 SP5+PP5

Intel® Intrinsics Guide也可以查询到每个函数所属的指令集和对应的头文件信息。

2 编译选项

使用 SSE/AVX 指令,除了包含头文件以外,还需要添加额外的编译选项,才能保证代码编译成功。各版本的 SSE 和 AVX 都有单独的编译选项,例如-msseN, -mavxN(N 表示版本编号)。此类编译选项支持向下兼容,比如-msse4可以编译 SSE2 的函数,-mavx也可以兼容各版本的SSE。

3 数据类型

Intel目前主要的SIMD指令集有 MMX, SSE, AVX, AVX-512,其对处理的数据位宽分别是:

  • MMX 64位
  • SSE 128位
  • AVX 256位
  • AVX-512 512位

每种位宽对应一个数据类型,数据类型名称包括三个部分:

  • 前缀 __m,两个下划线加 m。
  • 中间是数据位宽。
  • 最后加上的字母表示数据类型,i 为整数,d 为双精度浮点数,不加字母则是单精度浮点数。

那么,对于 SSE 指令集具有的数据类型:__m128, __m128i, __m128d
AVX 指令集则包括__m256, __m256i, __m256d;
__m64 对应 64 位对应的数据类型,该类型仅能供 MMX 指令集使用。由于 MMX 指令集也能使用 SSE 指令集的 128 位寄存器,故该数据类型使用的情况较少。

这里的位宽指的是 SIMD 寄存器的位宽,CPU 需要先将数据加载进专门的寄存器之后再并行计算。

4 Instrinsic 函数命名

Intrinsic 函数的命名通常由3个部分构成:

  • 第一部分为前缀_mm,MMX 和 SSE 都为_mm开头,AVX 和 AVX-512 则会额外加上 256 和 512 的位宽标识。
  • 第二部分表示执行的操作,比如_add, _mul, _load等,操作本身也会有一些修饰,比如_loadu表示以无需内存对齐的方式加载数据。
  • 第三部分为操作选择的数据范围和数据类型,比如_ps的 p(packed) 表示所有数据,s(single) 表示单精度浮点; _ss则表示 s(single) 第一个,s(single) 单精度浮点; _epixx(xx 为位宽)操作所有的 xx 位的有符号整数,_epuxx则是操作所有的 xx 位的无符号整数。

例如_mm256_load_ps表示将浮点数加载进整个 256 位寄存器中。

SSE 指令集对分支处理能力比较差,而且从 128 位的数据中提取某些元素数据的代价又比较大,因此不适合有复杂逻辑的运算。

绝大部分 Intrinsic 函数都是按照这样的格式构成,每个函数都能在Intel® Intrinsics Guide找到更为完整的描述。

5 SSE 指令基本使用

考虑这样的场景,需要对A、B数组求和,并将结果写入数组C。

常规的 C 写法:

#include <stdio.h>

#define SIZE 100000
int main()
{
    float A[SIZE], B[SIZE], C[SIZE];
    
    for(int i = 0; i < SIZE; i++)
        C[i] = A[i] + B[i];
}

使用 SSE 指令来优化,进行数据并行计算:

// 导入头文件
#include <intrin.h>

// 创建 3 个 __m128 寄存器分别存储三个数组的 float 值
__m128 ra, rb, rc;

// 到了循环体内,我们需要把 A、B 的值写入寄存器之中,这就需要用到 _mm_loadu_ps 函数,他会把从指针位置开始的后 128 位的数据写入寄存器。
// 这里使用 loadu 而不是用 load 函数,和内存对齐有关,后面再作说明
ra = _mm_loadu_ps(A + i);   // A+i 等价于 &A[i]。
rb = _mm_loadu_ps(B + i);

// 用 _mm_add_ps 函数计算 ra、rb 相加,然后把结果返回到 rc 之中。
rc = _mm_add_ps(ra, rb);

// rc 的值还得写回到C数组,这要用到 _mm_storeu_ps 函数。
// 同样是和内存对齐有关,这里使用 storeu,不使用 store
_mm_storeu_ps(C + i, rc);

// 因为128位寄存器一次可以写入 4(128/32) 个 float 值,等于一次循环计算 4 个 float 的加法,循环的跨步长也应该由 1 变为 4,这样循环次数就只需要原来的 1/4。
for (int i = 0; i < SIZE; i += 4)

完整的代码如下,代码编译之后即可运行,编译的时候需要加编译选项:

#include <stdio.h>
#include <intrin.h>

#define SIZE 100000
int main()
{
    float A[SIZE], B[SIZE], C[SIZE];

    for (int i = 0; i < SIZE; i += 4)    // 一次计算4个数据,所以要改成+4
    {
        __m128 ra = _mm_loadu_ps(A + i); // ra = {A[i], A[i+1], A[i+2], A[i+3]}
        __m128 rb = _mm_loadu_ps(B + i); // rb = {B[i], B[i+1], B[i+2], B[i+3]}
        __m128 rc = _mm_add_ps(ra, rb);  // rc = ra + rb
        _mm_storeu_ps(C + i, rc);        // C[i~i+3] <= rc
    }
}

这里的 SSE 的版本代码相比于前面纯 C 的版本,大概会有 1.7 倍左右的速度提升。

这里循环规模缩减为了纯 C 版本的 1/4,但是速度只提高了 1.7 倍,提升并没有循环缩减的规模大,这是因为上面的 SSE 版本代码里面用了非常多不必要的中间变量(ra、rb、rc都是),可以再做一些精简优化:

#include <stdio.h>
#include <intrin.h>

#define SIZE 100000
int main()
{
    float A[SIZE], B[SIZE], C[SIZE];

    for (int i = 0; i < SIZE; i += 4)
    {
        _mm_storeu_ps(C + i,  _mm_add_ps(_mm_loadu_ps(A + i), _mm_loadu_ps(B + i)));
    }
}

此时的代码速度提升有差不多 2.2 倍了,这距离 4 倍的循环规模缩减还有一段距离,还可以通过内存对齐,类型转换和 AVX 来进一步提高速度。

6 内存对齐

6.1 关于 loadu 和 load

loadu 表示无需内存对齐,不加 u 的版本需要原数据有 16 字节内存对齐,否则在读取的时候就会触发边界保护产生异常。

xx 字节对齐的意思是要求数据的地址是 xx 字节的整数倍,128 位宽的 SSE 要求 16 字节内存对齐,而 256 位宽的 AVX 函数则是要求32 字节内存对齐。

可以明显地看出,内存对齐要求的字节数就是指令需要处理的字节数,而要求内存对齐也是为了能够一次访问就完整地读到数据,从而提升效率。

6.2 内存对齐

创建变量时设置 N 字节对齐可以用:

  • __declspec(align(N)),MSVC 专用关键字
  • __attribute__((__aligned__(N))),GCC 专用关键字
  • alignas(N),C++11 关键字
    只需要在创建变量时在类型名前加上这几个关键字,就像下面这样:
alignas(16)                      float A[SIZE]; // C++11
__declspec(align(16))            float B[SIZE]; // MSVC
__attribute__((__aligned__(16))) float C[SIZE]; // GCC

对于 new 或 malloc 这种申请的内存也有相应的设置方法:

  • _aligned_malloc(size, N),包含在<stdlib.h>头文件中,与 malloc 相比多了一个参数 N 用于指定内存对齐。注意!用此方法申请的内存需要用_aligned_free()进行释放。
  • new((std::align_val_t) N),C++17 新特性,需要在 GCC7 及以上版本使用-std=c++17编译选项开启。

具体使用方式如下:

float *A = new ((std::align_val_t)32) float[SIZE];             // C++17
float *B = (float *)_aligned_malloc(sizeof(float) * SIZE, 32); // <stdlib.h>
_aligned_free(B);                                              // 用于释放_aligned_malloc申请的内存

使用关键字把数组进行 16 字节内存对齐后,就可以把 loadu 和 storeu 替换成 load 和 store。

#include <stdio.h>
#include <intrin.h>

#define SIZE 100000
int main()
{
    __attribute__((__aligned__(16))) float A[SIZE], B[SIZE], C[SIZE]; // GCC的内存对齐

    for (int i = 0; i < SIZE; i += 4)
    {
        _mm_store_ps(C + i,  _mm_add_ps(_mm_load_ps(A + i), _mm_load_ps(B + i))); // 用store和load替换storeu和loadu
    }
}

7 类型转换

_mm_load_ps函数和_mm_store_ps函数内部实际上只是对传入的指针进行了一次类型转换

/* Load four SPFP values from P.  The address must be 16-byte aligned.  */
extern __inline __m128 __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_load_ps (float const *__P)
{
  return *(__m128 *)__P;
}

/* Store four SPFP values.  The address must be 16-byte aligned.  */
extern __inline void __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_store_ps (float *__P, __m128 __A)
{
  *(__m128 *)__P = __A;
}

这里的类型转换*(__m128 *)__P可以分成两部分来看:

  • (__m128 *)__P:将__Pfloat *类型转换为__m128 *
  • *:访问__m128 *指针指向的__m128对象

这里我们直接在代码中实现这一步,就可以去掉这两个函数调用的成本:

#include <stdio.h>
#include <intrin.h>

#define SIZE 100000
int main()
{
    __attribute__((__aligned__(16))) float A[SIZE], B[SIZE], C[SIZE];

    for (int i = 0; i < SIZE; i += 4)
    {
        *(__m128 *)(C + i) = _mm_add_ps(*(__m128 *)(A + i), *(__m128 *)(B + i)); // 使用类型转换
    }
}

注:转换成__m128*同样是有内存对齐要求的,若是低于 16 字节对齐就会在访问指针时出错,非对齐的情况应该使用__m128_u*指针。

在前面的基础上,使用内存对齐和类型转换之后,相比于纯 C 的代码,速度提高到了 3.8 倍,这已经很接近循环缩减的 4 倍的期望值了。

8 AVX

AVX 的用法与 SSE 相同,只需要根据命名规律修改一下数据类型和函数的名称就可以了。AVX 的数据处理位宽为 256 位,内存对齐要求对齐到 32 字节。

#include <stdio.h>
#include <intrin.h>

#define SIZE 100000
int main()
{
    __attribute__((__aligned__(32))) float A[SIZE], B[SIZE], C[SIZE]; // 32字节对齐

    for (int i = 0; i < SIZE; i += 8) // 循环跨度修改为8
    {
        *(__m256 *)(C + i) = _mm256_add_ps(*(__m256 *)(A + i), *(__m256 *)(B + i)); // 使用256位宽的数据与函数
    }
}

使用 AVX,加上内存对齐和类型转换之后,相比于纯 C 的代码,速度提高到了 7 倍。

9 整数操作

SSE/AVX 整数计算使用的数据类型是__m128i/__m256i,二者都是以i结尾。

还有基本的算数函数,比如 SSE 的加法addepi表示整数,后面的数字就是单个整数的数据位宽。比如epi8就是 1 字节 char 加法, 4 字节 int 加法就是epi32

__m128i _mm_add_epi8 (__m128i a, __m128i b)
__m128i _mm_add_epi16 (__m128i a, __m128i b)
__m128i _mm_add_epi32 (__m128i a, __m128i b)
__m128i _mm_add_epi64 (__m128i a, __m128i b)

整数乘法,以 SSE 为例:

// mul
__m128i _mm_mul_epi32 (__m128i a, __m128i b)
__m128i _mm_mul_epu32 (__m128i a, __m128i b)
// mullo
__m128i _mm_mullo_epi16 (__m128i a, __m128i b)
__m128i _mm_mullo_epi32 (__m128i a, __m128i b)
__m128i _mm_mullo_epi64 (__m128i a, __m128i b)
// mulhi
__m128i _mm_mulhi_epi16 (__m128i a, __m128i b)
__m128i _mm_mulhi_epu16 (__m128i a, __m128i b)

有三种不同功能的乘法,建议使用之前到Intel® Intrinsics Guide查看一下功能描述。

标签:__,ps,AVX,mm,指令,m128i,SSE,SIZE
From: https://blog.csdn.net/qq_38342510/article/details/141576247

相关文章

  • Vue(2)——Vue指令
    目录v-htmlv-show和v-ifv-else和v-else-ifv-onv-bindv-for v-modelv-html设置元素的innerHTML<body><divid="app"><divv-html="msg"></div></div><scriptsrc="https://cdn.jsdelivr.net/np......
  • 大白话【8】WindowsServer2016搭建DNS服务
    1.DNS服务功能介绍2.DNS服务器搭建2.0准备环境2.1把该DNS服务器设置成静态IP2.2修改主机名(可省略)2.3安装DNS服务DNS服务器名为www;IP为192.168.2.1003.客户机测试在网内可网络连通的客户机如何验证DNS服务器域名解析有效性?3.1可以ping不通,只要看到解析就行。......
  • v-bind指令与class类和style的内联样式的绑定
    1.v-bind指令1.v-bind的认知与用法我们先创建一个新的页面去除掉一些元素保留剩下的元素然后对其添加一些属性和值创建好了之后我们可以来在页面上显示一个图片用我们的image组件找到我们的本地图片的位置或者网络图片的url放入我们的src里面。在src里面用到了我们的图片......
  • golang interface{} Type assertions类型断言 x.(T) 和Type switches类型选择 switch
    在golang的开发中,我们经常会用到类型断言typeassertions和switchx.(type)类型选择,他们都可以对interface{}空接口类型的数据进行类型断言,他们的功能类似但是有区别,区别如下:共同点:都可以对interface{} /any类型的数据进行数据类型的断言区别:  类型断言x.(T)......
  • Chapter 03 Vue指令(下)
    欢迎大家订阅【Vue2+Vue3】入门到实践专栏,开启你的Vue学习之旅!文章目录前言一、v-on指令二、v-for指令三、v-bind指令四、v-model指令前言在Vue.js中,指令是带有v-前缀的特殊属性,不同属性对应不同的功能。通过学习不同的指令,我们能够灵活应对多种业务场景......
  • 彩度战队运行故障:Assembly-CSharp.dll文件缺失原因及修复方法
    一、缺失原因Assembly-CSharp.dll文件是Unity游戏引擎在编译C#脚本时生成的一个动态链接库(DLL)文件,它包含了游戏或软件的核心逻辑和控制代码,是游戏或软件能够正常运行的关键部分。在彩度战队游戏中,Assembly-CSharp.dll文件缺失可能由以下原因造成:安装不完整或损坏:游戏在安......
  • 表达式用法,ref定义响应式,v-bind指令和图片轮播结合,class和style内联样式绑定,事件监听
    表达式用法当前时间,随机数,返回值,判断取值ref响应式使用ref赋值和普通赋值v-bind指令和图片轮播结合(v-bind可以省略成":")class和style内联样式绑定数据绑定一个常见需求是操作元素的class列表和它的内联样式两个class会用到这两个的样式,用v-bind对class里面的......
  • Java 调用电脑中安装的tesseract.exe进行图片中的文本方向识别
    StringimagePath="测试图片完整地址";StringtesseractPath="Tesseract安装路径\\tesseract.exe";ProcessBuilderprocessBuilder=newProcessBuilder(tesseractPath,imagePath,"stdout","--......
  • Chapter 02 Vue指令(上)
    欢迎大家订阅【Vue2+Vue3】入门到实践专栏,开启你的Vue学习之旅!文章目录前言一、v-text指令二、v-html指令三、v-show指令四、v-if指令五、v-else指令六、v-else-if指令前言在Vue.js中,指令是带有v-前缀的特殊属性,不同属性对应不同的功能。通过学习不同的指......
  • 一文带你读懂vue3中directive指令的那些事
    概述vue3中内置了很多丰富实用的指令,如v-show、v-if/v-else或v-model等,但是实际开发中可能我们还需要某些统一的处理,比如交互按钮的防抖,输入框的自动focus等,这时我们就可以通过vue3的directive注册自定义指令。指令指令钩子vue3的自定义指令通常情况下是由一个包含类似......