SSE学习笔记

SSE学习笔记背景什么是指令集 指令集是为了增强 CPU 在某些方面 如多媒体 的功能而特意开发出的一组程序代码集合 常见的指令集有哪些呢 MMX Multi MediaExtensi 多媒体扩展 Intel1996 年推出的一项多媒体指令增强技术 共包含 57 条多媒体指令 这些指令一次可以处理多个数据 MMX 的主要问题是 CPU 无法同时处理浮点和 SIMD 数据 只对整数起作用 不支持浮点计算

背景

什么是指令集?

指令集是为了增强CPU在某些方面(如多媒体)的功能而特意开发出的一组程序代码集合。

常见的指令集有哪些呢?

MMX(Multi-Media Extensions,多媒体扩展):Intel1996年推出的一项多媒体指令增强技术。共包含57条多媒体指令,这些指令一次可以处理多个数据。MMX的主要问题是,CPU无法同时处理浮点和SIMD数据,只对整数起作用(不支持浮点计算)。

SSE指令集(Streaming SIMD Extensions,单指令多数据留扩展)兼容MMX指令,它可以通过SIMD(单指令多数据技术)和单时钟周期并行处理多个浮点来有效地提高浮点运算速度。SSE数据集包含70条指令,其中有50条SIMD(单指令多数据技术)浮点运算指令,12条MMX整数运算增强指令,8条优化内存中连续数据块的传输指令。理论上这些指令对目前流行的图像处理、浮点处理、3D运算、视频处理、音频处理等多媒体应用起到全面强化的作用。  注意:SSE指令和3DNow!指令彼此互不兼容,但SSE包含了3DNow!的绝大多数功能。

SSE2SSE3SSE4是SSE的扩展技术。

3DNow!指令集。

X86指令集

AVX指令集(Advanced Vector Extensions),Intel AVX指令集在SIMD计算性能增强的同时也沿用了的MMX/SSE指令集。不过和MMX/SSE的不同点在于增强的AVX指令,从指令的格式上就发生了很大的变化。AVX 指令集架构的改进和增强的功能:

  • 128 位 SIMD 寄存器 xmm0 – xmm15 扩展为 256 位的 ymm0 – ymm15 寄存器 ;
  • 支持 256 位的矢量运算,由原来 128 位扩展为 256 位 ;
  • 指令可支持最多 4 个操作数,实现目标操作数无需损毁原来的内容 ;
  • 引进新的 AVX 指令,非 Legacy SSE 指令移植 ;
  • 新增 FMA 指令子集进行 fused multiply-add/subtract 类运算,用子式表达为:± (a * b) ± c ;
  • 引进新的指令编码模式,使用 VEX prefix 来设计指令编码 。

SSE介绍

SSE(为 Streaming SIMD Extensions 的缩写)是由Intel公司,在 1999 年推出 Pentium III 处理器时,同时推出的新指令集,它是SIMD指令集扩展。SIMD(single-instruction, multiple-data)是一种使用单道指令处理多道数据流的CPU执行模式,即在一个CPU指令执行周期内用一道指令完成处理多个数据的操作。 当对多个数据对象执行完全相同的操作时, SIMD 指令可以大大提高性能。典型的应用是数字信号处理和图形处理。

SSE 指令包括了四个主要的部份:单精度浮点数运算指令整数运算指令(此为 MMX 之延伸,并和 MMX 使用同样的缓存器)、Cache 控制指令、和状态控制指令。 这里主要是介绍浮点数运算指令和 Cache 控制指令。

SSE新增的寄存器(用于浮点运算指令)

SSE 新增了八个新的 128 位缓存器,xmm0 ~ xmm7。 这些 128 位的缓存器,可以用来存放四个 32 位的单精度浮点数。 SSE 的浮点数运算指令就是使用这些缓存器。 和之前的 MMX 或 3DNow! 不同,这些缓存器并不是原来己有的缓存器(MMX 和 3DNow! 均是使用 x87 浮点数缓存器),所以不需要像 MMX 或 3DNow! 一样,要使用 x87 指令之前,需要利用一个EMMS 指令来清除缓存器的状态。 因此,不像 MMX 或 3DNow! 指令,SSE 的浮点数运算指令,可以很自由地和 x87 指令,或是 MMX 指令共享。 但是,这样做的主要缺点是,因为多任务操作系统在进行 context switch 时,需要储存所有缓存器的内容。 而这些多出来的新缓存器,也是需要储存的。 因此,既存的操作系统需要修改,在 context switch 时,储存这八个新缓存器的内容,才能正确支持 SSE 浮点运算指令。

下图是 SSE 新增的缓存器的示意图:

SSE_register

SSE新的数据类型

根据上面知道,SSE新增的寄存器是128bit的,那么SSE就需要使用128bit的数据类型 (也就是 __m128),SSE使用4个浮点数(4*32bit)组合成一个新的数据类型,用于表示128bit类型,SSE指令的返回结果也是128bit的。

SSE浮点运算指令分类

SSE的浮点运算指令分为两大类:packed 和 scalar。(有些地方翻译为“包裹指令和”“标量指令” :) )
packed指令是一次对XMM暂存器中的四个浮点数(即DATA0 ~ DATA3)均进行计算,而scalar则只对XMM暂存器中的DATA0进行计算。如下图所示:

see-ops

SSE指令格式

  • 第一部分表示指令的作用,比如加法add;
  • 第二部分是p或者s,分别表示为packed或者scalar;
  • 第三部分为s,表示单精度浮点数。

SSE学习笔记

SSE定址/寻址方式

SSE 指令和一般的x86 指令很类似,基本上包括两种定址方式:寄存器-寄存器方式(reg-reg)和寄存器-内存方式(reg-mem):

  • addps xmm0, xmm1 ;    reg-reg
  • addps xmm0, [ebx] ;      reg-mem

指令的运算结果会覆盖到第一个参数中。 例如,以上面的第一个例子来说,xmm0 缓存器会存放最后计算的结果。

另外,绝大部份需要存取内存的 SSE 指令,都要求地址是 16 的倍数(也就是对齐在 16 bytes 的边上)。 如果不是的话,就会导致 exception。 这是非常重要的。 因为,一般的 32 位浮点数只会对齐在 4 bytes 或 8 bytes 的边上(根据 compiler 的设定而不同)。 另外,若是处理数组中的数字,也需要特别注意这个问题。

支持SSE指令集的intrinsics内联函数

SSE指令的使用

要在 C 或 C++ 程序中使用SSE指令有两种方式:一是直接在C/C++中嵌入(汇编)指令(内嵌式汇编语言);二是使用Intel C++ Compiler或是Microsoft Visual C++中提供的支持SSE指令集的intrinsics内联函数。从代码可读和维护角度讲,推荐使用intrinsics内联函数的形式。以下是一些例子:

/ 内嵌式汇编语言使用SSE指令集 / _asm addps xmm0, xmm1 __asm movaps [ebx], xmm0 ... __m128 data; ... __asm { lea ebx, data addps xmm0, xmm1 movaps [ebx], xmm0 } / 通过 intrinsics内联函数使用SSE指令集 / __m128 data1, data2; ... __m128 out = _mm_add_ps(data1, data2); ... 

什么是intrinsics?  

intrinsics函数是对MMX、SSE等指令集的一种封装,以函数的形式提供,使得程序员更容易编写和使用这些高级指令,在编译的时候,这些函数会被内联为汇编,不会产生函数调用的开销。SSE指令中intrinsics函数的数据类型为:__m128,如果使用sizeof(__m128)计算该类型大小,结果为16,即等于四个浮点数长度。

SSE的 intrinsics函数 一般格式为:_mm_

_

  • 前缀_mm,表示是SSE指令集对应的Intrinsic函数;
  • < opcode> 是对应的指令操作,如_add,_mul,_load等,有些操作可能会有修饰符,如loadu将未16位对齐的操作数加载到寄存器中。

  • 为操作的对象名及数据类型。 在 SSE 浮点运算指令中,只有两个种类:
    ps
    ss。 其中,ps 是指
    Packed Single-precision,也就是这个指令对缓存器中的四个单精度浮点数进行运算;ss 则是指
    Scalar Single-precision,也就是这个指令只对缓存器中的 DATA0 进行运算。

所以,像上面的 _mm_add_ps 指令,就是把两个四维向量相加的指令。

SSE指令中的intrinsics函数的数据类型为:__m128,正好对应了上面提到的SSE新的数据类型(128bit),当然,这种数据类型只是一种抽象表示,实际是要转换为基本的数据类型的。

头文件

有了头文件,在我们的代码中才能调用指令集函数(加入头文件可以将汇编形式的指令集封装成C语言形式,可增强可读性以及可维护性),Visual Studio使用SSE需要添加对应的头文件:

  • mmintrin.h——>MMX
  • xmmintrin.h——>SSE
  • emmintrin.h——>SSE2
  • pmmintrin.h——>SSE3

SSE指令的内存对齐要求

SSE中大部分指令要求地址16bytes对齐的,要理解这个问题,以_mm_load_ps函数来解释,这个函数对应于loadps的SSE指令。

其原型为:extern __m128 _mm_load_ps(float const*_A);

可以看到,它的输入是一个指向float的指针,返回的就是一个__m128类型的数据,从函数的角度理解,就是把一个float数组的四个元素依次读取,返回一个组合的__m128类型的SSE数据类型,从而可以使用这个返回的结果传递给其它的SSE指令进行运算,比如加法等;从汇编的角度理解,它对应的就是读取内存中连续四个地址的float数据,将其放入SSE新的暂存器(xmm0~8)中,从而给其他的指令准备好数据进行计算。其使用示例如下:

float input[4] = { 1.0f, 2.0f, 3.0f, 4.0f }; __m128 a = _mm_load_ps(input);

这里加载正确的前提是:input这个浮点数阵列都是对齐在16 bytes的边上。否则加载的结果和预期的不一样。如果没有对齐,就需要使用 _mm_loadu_ps 函数,这个函数用于处理没有对齐在16bytes上的数据,但是其速度会比较慢。关于内存对齐的问题,这里就不详细讨论什么是内存对齐了,以及如何指定内存对齐方式。这里主要提一下,SSE的intrinsics函数中的扩展的方式:

一般来说,宣告一个 float 的数组,并不会对齐在 16 bytes 的边上。 如果希望它会对齐在 16 bytes 的边上,以便利用 SSE 指令的话,Visual C++ 6.0 Processor Pack 和 Intel C++ compiler 都可以指定对齐方式。 指定的方式如下:

  • __declspec(align(16)) float input[4];

这样就可以直接用较快的 _mm_load_ps 来加载数据了。 因为 SSE 浮点数指令常常需要数据对齐在 16 bytes 的边上,所以 xmmintrin.h 也定义了一个宏 _MM_ALIGN16, 是同样的意义。 因此,上面的程序也可以写成:

  • _MM_ALIGN16 float input[4];

【注意】GCC编译器和VC编译器下字节对齐是不同的,例如创建此结构体实例时按16字节对齐:

  • gcc: __attribute__((aligned(16)))
  • vc:   __declspec(align(16))
     

大小端问题

这个只是使用SSE指令的时候要注意一下,我们知道,x86的little-endian特性,位址较低的byte会放在暂存器的右边。也就是说,若以上面的input为例,在载入到XMM暂存器后,暂存器中的DATA0会是1.0,而DATA1是2.0,DATA2是3.0,DATA3是4.0。如果需要以相反的顺序载入的话,可以用_mm_loadr_ps 这个intrinsic,根据需要进行选择。

 

【参考】

          http://dev.gameres.com/Program/Other/SSEjianjie.htm

          https://blog.csdn.net/gengshenghong/article/details/

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/201227.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月20日 上午9:48
下一篇 2026年3月20日 上午9:48


相关推荐

  • mysql表注释乱码[通俗易懂]

    mysql表注释乱码

    2022年2月10日
    44
  • 深入解析:【仿生机器人】基于 GPT-SoVITS 的 发声器

    深入解析:【仿生机器人】基于 GPT-SoVITS 的 发声器

    2026年3月16日
    4
  • CODEIF_变量取名「建议收藏」

    CODEIF_变量取名「建议收藏」网址https://unbug.github.io/codelf/大部分开发者都或多或少遇到过变量命名的烦恼,如果命名不规范,不仅会影响开发的效率,而且对后面维护的同学来说也是一个不小的挑战,因为他要去揣摩你这个变量的含义。“计算机科学里两件最难的事:缓存失效和命名。”Codelf通过搜索在线开源平台Github,Bitbucket,GoogleCode,Codeplex,Sou…

    2022年4月19日
    440
  • 决策树算法例题_决策树算法比较

    决策树算法例题_决策树算法比较上一节介绍了决策树的一些基本概念,诸如树的基本结构,信息熵等。这次就用一个例子,来看看ID3决策树的具体运行过程吧~~

    2022年8月6日
    10
  • 详解Linux双网卡绑定之bond0「建议收藏」

    1、什么是bond?  网卡bond是通过多张网卡绑定为一个逻辑网卡,实现本地网卡的冗余,带宽扩容和负载均衡,在生产场景中是一种常用的技术。Kernels2.4.12及以后的版本均供bonding模块,以前的版本可以通过patch实现。2、实现原理:  网卡工作在混杂(promisc)模式,接收到达网卡的所有数据包,tcpdump工作用的也是混杂模式(promisc),将两块网卡的MAC地址…

    2022年4月1日
    67
  • MutationObserver详解

    MutationObserver详解MutationObse 用来监视 DOM 变动 DOM 的任何变动 比如节点的增减 属性的变动 文本内容的变动都会触发 MutationObse 事件 但是 它与事件有一个本质不同 事件是同步触发 也就是说 DOM 的变动立刻会触发相应的事件 MutationObse 则是异步触发 DOM 的变动并不会马上触发 而是要等到当前所有 DOM 操作都结束才触发 MutationObse 有以下特点 它等待所有脚本任务完成后 才会运行 即异步触发方式 它把 DOM 变动

    2026年3月17日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号