一、SIMD简介
IA-32的SIMD架构当前由4种技术构成:
- 多媒体扩展(Multimedia Extension,MMX)
- 流化SIMD扩展(Streaming SIMD Extension, SSE)
- 流化SIMD扩展第二实现(Streaming SIMD Extension Second Implementation, SSE2)
- 流化SIMD扩展第三实现(Streaming SIMD Extension third Implementation,SSE3)
( 具体可以参考:http://blog.youkuaiyun.com/conowen/article/details/7255920)
SIMD技术的主要好处是它为程序员提供使用单一指令执行并行数学操作的能力。MMX和SSE架
构提供可以保存打包数据的附加寄存器(多个数据值在单一寄存器中)。MMX和SSE指令具有一次
对寄存器中所有打包数据元素执行单一数学操作的能力。
1、MMX
MMX技术的主要目的是对integer数据类型执行SIMD操作。MMX的SIMD架构提供3种附加的
整数值类型:
- 64位打包字节整数(包含8个单字节整数值)
- 64位打包字整数(包含4个字整数值)
- 64位打包双字整数(包含2个双字整数值)
因为MMX整数数据类型使用64位,所以不能使用一般的通用寄存器保存它们。替换的做法
是,MMX技术利用80位FPU寄存器执行其所有的数学操作。MMX寄存器被命名为MM0到
MM7。它们直接映射到FPU寄存器R0到R7。但和FPU寄存器不同,MMX寄存器是静态的,它们
不能作为堆栈使用。MM0寄存器用于引用FPU寄存器R0。把数据值存放在MM0寄存器中不会把
前一个值移动到MM1寄存器。FPU堆栈寄存器顶部值不会对MMX指令有任何影响。
虽然FPU寄存器用于保存MMX数据,但是它们也用于保存FPU数据。这可能造成非常大的混
乱。在MMX模式下使用这些寄存器处理MMX数据(寄存器的指数值设置为全1),在FPU模式下
使用寄存器处理一般的FPU扩展双精度浮点数值。
不幸的是,在MMX模式下使用FPU寄存器时,FPU的标记寄存器会被破坏。为了解决这个问
题,最好把使用FPU寄存器的指令和使用MMX寄存器的指令分开。解决方案是使用FSAVE或
FXSAVE指令把FPU寄存器保存到内存中,当MMX指令执行完成时使用FRSTOR或FXRSTOR
指令恢复寄存器。FSAVE和FRSTOR指令仅仅保存FPU状态。FXSAVE和FXRSTOR指令保存
FPU、MMX和SSE状态。
MMX操作执行完毕时,应该使用EMMS指令清除FPU的标记寄存器值,以便确保FPU指令
正确地执行。
64位打包整数值保存多个整数值。MMX架构包含附加指令,用于支持在单一指令内处理多个
整数值(因此有了“单指令多数据”这个术语)。
2、SSE
SSE技术主要目的是对浮点数据执行SIMD操作。SSE架构提供另一种新的数据类型:128位
打包的单精度浮点数据类型。这个数据类型用于包含4个单精度浮点值。
因为新的数据类型需要128位,所以为进行SSE处理创建了新的寄存器集合。这些新的寄存
器包括8个128位寄存器(XMM0到XMM7),使用它们保存了128位打包的单精度浮点数据值。
SSE浮点数据操作使用XMM寄存器。
SSE架构也包含新的指令,用于对打包的单精度浮点值执行SIMD数学操作。这样最多可以
在单一操作中进行4个浮点运算。
3、SSE2
SSE2架构通过添加5种新的数据类型扩展了SSE核心架构:
- 128位打包双精度浮点值(包含2个双精度值)
- 128位打包字节整数值(包含16个单字节整数值)
- 128位打包字整数值(包含8个字整数值)
- 128位打包双字整数值(包含4个双字整数值)
- 128位打包四字整数值(包含2个四字整数值)
所有这些新的数据类型都使用128位的XMM寄存器保存要处理的数据。SSE2技术提供附加
的浮点和整数SIMD操作。它包含对打包数据执行数学运算的附加指令(同样使用单一操作)。
SSE3架构没有在SSE2架构的基础上添加任何附加的数据类型。它为SSE2数据类型的高级
处理提供了新的指令。
二、检测支持的SIMD操作
1、检测支持
CPUID指令生成处理器的厂商ID。也可以使用CPUID指令生成处理器的其他信
息。这个指令生成的信息的类型由执行CPUID指令时EAX寄存器的值控制。
当EAX寄存器的值为1时,CPUID指令返回处理器签名信息。处理器签名信息包括两个包含
处理器特性标志的寄存器。ECX和EDX寄存器包含的位表示不同特性在处理器上是否可用,
可以使用TEST指令把CPUID指令返回的ECX和EDX寄存器中的值和表示特性标志的设置值
进行比较。
2、SIMD特性程序
.section .data
gotmmx:
.asciz "Supports MMX"
gotsse:
.asciz "Supports SSE"
gotsse2:
.asciz "Supports SSE2"
gotsse3:
.asciz "Supports SSE3"
gotssse3:
.asciz "Supports sSSE3"
gotsse41:
.asciz "Supports SSE4_1"
gotsse42:
.asciz "Supports SSE4_2"
output:
.asciz "%s\n"
.section .bss
.lcomm ecxdata, 4
.lcomm edxdata, 4
.section .text
.globl _start
_start:
movl $1, %eax
cpuid
movl %ecx, ecxdata
movl %edx, edxdata
test $0x00800000, %edx
jz done
pushl $gotmmx
pushl $output
call printf
addl $8, %esp
movl edxdata, %edx
test $0x02000000, %edx
jz done
pushl $gotsse
pushl $output
call printf
addl $8, %esp
movl edxdata, %edx
test $0x04000000, %edx
jz done
pushl $gotsse2
pushl $output
call printf
addl $8, %esp
movl ecxdata, %ecx
test $0x00000001, %ecx
jz done
pushl $gotsse3
pushl $output
call printf
addl $8, %esp
movl ecxdata, %ecx
test $0x00000200, %ecx
jz done
pushl $gotssse3
pushl $output
call printf
addl $8, %esp
movl ecxdata, %ecx
test $0x00080000, %ecx
jz done
pushl $gotsse41
pushl $output
call printf
addl $8, %esp
movl ecxdata, %ecx
test $0x00100000, %ecx
jz done
pushl $gotsse42
pushl $output
call printf
addl $8, %esp
done:
pushl $0
call exit
三、使用MMX指令
为了在汇编语言程序中利用MMX架构,必须执行下列步骤:
- 从整数值创建打包整数值
- 把打包整数值加载到MMX寄存器中
- 对打包整数值执行MMX数学操作
- 从MMX寄存器获得结果,存放到内存位置中
1、加载和获得打包的整数值
使用MOVQ指令把整数值传送进和传送出MMX寄存器。
整数值必须按照打包整数格式存放到MMX寄存器中。MMX打包整数类型可以用于
8个字节整数值,4个字整数值或者2个双字整数值。可以把这些值存放在内存位置
中,以便加载到MMX寄存器中,如下:
.section .data
packedvalue1:
.byte 10, 20, -30, 40, 50, 60, -70, 80
packedvalue2:
.byte 10, 20, 30, 40
packedvalue3:
.int 10, 20
.section .text
.globl _start
_start:
movq packedvalue1, %mm0
movq packedvalue2, %mm1
movq packedvalue3, %mm2
也可以在.bss段中为每个值保留8字节的内存,然后通过程序把值存放到内存位置中。类似
地,可以使用MOVQ指令把数据从MMX寄存器传送到64位内存位置中。
2、执行MMX操作
把数据加载到MMX寄存器中之后,就可以使用单一指令对打包数据执行并行操
作。利用同样存放的打包整数值,对寄存器中的每个打包整数值执行操作。
1)MMX加法和减法指令
使用通用寄存器的一般整数加法和减法是非常简单明了的操作。但是,当使用打
包整数数学操作时有一个问题。
对于通用寄存器的一般加法和减法,如果操作出现溢出的情况,就设置EFLAGS寄存器表示
溢出。对于打包整数值操作,同时计算多个结果值。这意味着和一般整数运算不同,单一一组标
志不能表示操作的结果。
替换做法是,使用MMX加法或者减法时,必须提前做出决定在处理器遇到操作中发出溢出的
情况时应该进行什么操作。可以以执行数学操作的3种溢出方法中进行选择:
- 环绕
- 带符号饱和运算
- 无符号饱和运算
环绕运算的方法执行数学操作。并且允许溢出环绕数据长度。这种方法假设将在执行操作之
前进行检查,确保不会出现溢出情况。如果出现了,就会截断结果值,删除所有进位值。
带符号和无符号饱和运算的方法把溢出情况的结果设置为预先设置的值,这取决于使用的打
包整数值的长度,以及溢出的符号。
如果存在正溢出的情况,就把结果设为数据类型的最大值。如果存在负溢出情况,就把结
果设置为数据类型的最小值。虽然从数学运算的角度看来,饱和运算的溢出值也许没有什么意
义,但是这样设置是有原因的。MMX技术的主要应用之一是执行图形计算以便显示图片。当计算
像素的红、蓝和绿值时,正溢出应该把像素设置为最大值,就是白色。对于负溢出的情况,像素
被设置为最小值,就是黑色。
下面是MMX数学操作:
每个MMX数学操作都具有相同的格式:
PADDSB source, destination
/*其中source可以是MMX寄存器或者64位内存位置,destination是MMX寄存器。*/
例如,
PADDSB %mm1, %mm0
/*把寄存器MM0的内存和MM1的内容相加,把结果存放到MM0寄存器中*/
.section .data
value1:
.int 10, 20
value2:
.int 30, 40
.section .bss
.lcomm result, 8
.section .text
.globl _start
_start:
movq value1, %mm0
movq value2, %mm1
paddd %mm1, %mm0
movq %mm0, result
movl $1, %eax
movl $0, %ebx
int $0x80
2)MMX乘法指令
MMX整数乘法有些困难。因为乘数乘法可能生成比输入操作数大得多的值,所以
MMX乘法允许使用两条指令完成乘法操作。第一条指令PMULL对每对打包字整数值
相乘,把结果的低16位存放到目标寄存器中。
然后可以把结果的低16位传送到适当的内存位置,然后把原始的操作数重新加载到两个输
入寄存器中。下面,使用PMULH指令,它使打包字整数值相乘,把结果的高16位存放到目标寄
存器中。现在拥有了乘法完整结果的低位和高位。
PMULL和PMULH指令有对应于带符号整数值(PMULLW和PMULHW)和无符号整数值
(PMULLUW和PMULHUW)的版本。
PMADDWD指令是一个专用指令。它对源操作数中的4个带符号字整数值和目标操作数中
的4个带符号字整数值执行乘法操作。这样生成4个带符号双字整数值。然后把相邻的双字整数值
相加,生成2个双字整数结果值。
3)MMX逻辑和移位指令
MMX架构也提供对于四字值执行一般布尔逻辑操作和位移位操作的指令。
逻辑指令的格式如下:
PAND source, destination
/*其中source可以是MMX寄存器或者64位的内存位置,destination必须是MMX寄存器。
左移位指令可以使用字、双字或者四字操作数;还有要移位的位置数量;还有要移位的
位置数量;右移位指令可以使用字或者双字操作数,还要移位的位置数量*/
4)MMX比较指令
MMX架构也包含用于比较两个值的比较指令。
MMX比较指令和一般的CMP指令有些不同。因为MMX数据类型保存多个值,所以
不为相等或者大于或者小于设置标志。
替换做法是,源和目标打包整数值进行并交,把结果存放到目标打包整数值中。如果打包整数
值对满足比较条件(要么相等,要么目标值大于源值),就把结果打包整数值设置为全1。如果不
满足条件,就把结果值设置为全0。
因为第二对和第三对打包字整数值相等,所以结果中的这些值被设置为1。其他两个打包字整
数值被设置为0。
.section .data
value1:
.short 10, 20, -30, 40
value2:
.short 10, 40, -30, 45
.section .bss
.lcomm result, 8
.section .text
.globl _start
_start:
movq value1, %mm0
movq value2, %mm1
pcmpeqw %mm1, %mm0
movq %mm0, result
movl $1, %eax
movl $0, %ebx
int $0x80
四、使用SSE指令
SSE架构提供对打包精度浮点值的SIMD支持。和MMX架构一样,SSE提供新的
指令,用于把数据传送到XMM寄存器中,对SSE数据执行数学操作以及从XMM寄存
器获得数据。
SSE和MMX的一个区别是每个SSE指令都有两个版本。第一个版本使用后缀
PS。这些指令对打包单精度浮点值执行类似于MMX操作的运算操作——打包数据值
中的每个值都参与操作,并且产生的打包值包含每个打包值操作的结果。
第二个版本使用SS后缀。对一个标量单精度浮点数值执行运算操作。这些指令不对打包值中
的所有浮点值执行操作,而只对打包值中的低位双字执行。源操作数中剩余的3个值直接传送给结
果。
标量操作允许对XMM寄存器中的一个单精度浮点值执行一般的FPU的运算操作。
1、传送数据
从下表可以看出,传送单精度浮点值在很大程度上依赖于值是否在内存中对准了。MOVAPS
指令要求数据在内存中对准16字节边界。这使处理器更容易在单一操作中读取数据。
.align命令指示gas汇编器把数据对准特定的内存边界。它使用单一操作数——数据要对准的
内存边界的长度。SSE的MOVAPS指令期望数据位于16字节的内存边界,所以指令如下:
.section .data
.align 16
value1:
.float 12.34, 2345.543, -3493.2, 0.4491
.section .text
.globl _start
_start:
movaps value1, %xmm0
2、处理数据
1)运算指令
这些指令都使用两个操作数:源操作数,它可以是128位内存或者XMM寄存器;目标操作数,
它必须是XMM寄存器。
.section .data
.align 16
value1:
.float 12.34, 2345, -93.2, 10.44
value2:
.float 39.234, 21.4, 100.94, 10.56
.section .bss
.lcomm result, 16
.section .text
.globl _start
_start:
movaps value1, %xmm0
movaps value2, %xmm1
addps %xmml, %xmm0
sqrtps %xmm0, %xmm0
maxps %xmm1, %xmm0
movaps %xmm0, result
movl $1, %eax
movl $0, %ebx
int $0x80
两个打包值存入XMM0和XMM1。
两个打包值相加,结果存入XMM0。
使用单一指令计算XMM0中4个值的平方根。
确定两个打包值的最大值。
并把结果传入result的内存位置。
2)比较指令
SSE的比较指令单独地比较128位打包单精度浮点值的每个元素。结果是一个掩码,满足比
较条件的结果全1的值,不满足条件的结果是全0的值。(这些指令的标量版本只对最低的双字值
执行)。
UCOMISS指令可以比较包含特殊值(比如非数字(not-a-number,NaN)值)的单精度浮
点值。
CMPPS指令有些不同。它可以使用所有比较类型(等于、小于、大于等待)比较两个打包
值。基本CMPPS指令有3个操作数:
CMPPS imp, source, destination
需要source和destination操作数(目标操作数必须是XMM寄存器,但是源操作数可以是
128位内存或者XMM寄存器)。奇怪的地方是imp操作数,这个操作数称为实现操作数
(implementation operand),它确定指令将执行什么类型的比较。它是一个无符号整数值,可
以把它编码为立即数或者变量,这个操作数的值确定比较类型。
因此确定两个XMM寄存器是否相等,可以使用下面的指令:
cmpps $0, %xmm1, %xmm0
结果将是位掩码(全1表示相等,全0表示值不等)。存放在寄存器XMM0中。
有序和无序用于发现不是合法的浮点数子表示的数据值。当至少一个不是合法浮点数字
时,无序比较结果为真。仅当两个操作数都使合法浮点数字时,有序比较结果为真。
汇编器gas提供了代替implementation操作数的伪指令。
下面演示如何使用CMPEQPS指令:
.section .data
.align 16
value1:
.float 12.34, 2345., -93.2, 10.44
value2:
.float 12.34, 21.4, -93.2, 10.45
.section .bss
.lcomm result, 16
.section .text
.globl _start
_start:
movaps value1, %xmm0
movaps value2, %xmm1
cmpeqps %xmm1, %xmm0
movaps %xmm0, result
movl $1, %eax
movl $0, %ebx
int $0x80
3)SSE整数指令
SSE架构还提供了处理64位打包整数值的一些扩展特性。
五、使用SSE2指令
SSE2指令使用128位XMM寄存器保存2个双精度浮点值、4个双字整数值或者2个
四字整数值。也提供数学操作以及标量操作。
1、传送数据
SSE2指令提供传送对准和不对准的数据的功能。为了使用MOVAPD和MOVDQA指令,内存
中的数据必须位于位于16字节的边界:
.section .data
.align 16
packedvalue1:
.double 10.235, 289.1
packedvalue2:
.int 10, 20, 30, 40
.section .text
.globl _start
_start:
movapd packedvalue1, %xmm0
movdqa packedvalue2, %xmm1
2、处理数据
SSE2指令集提供用于处理打包双精度浮点值、打包字整数值、打包双字整数值和打包字整
数值的数学指令。
.section .data
.align 16
value1:
.double 10.42, -5.330
value2:
.double 4.25, 2.10
value3:
.int 10, 20, 30, 40
value4:
.int 5, 15, 25, 35
.section .bss
.lcomm result1, 16
.lcomm result2, 16
.section .text
.globl _start
_start:
movapd value1, %xmm0
movapd value2, %xmm1
movdqa value3, %xmm2
movdqa value4, %xmm3
mulpd %xmm1, %xmm0
paddd %xmm3, %xmm2
movapd %xmm0, result1
movdqa %xmm2, result2
movl $1, %eax
movl $0, %ebx
int $0x80
六、SSE3指令
SSE3添加了几条指令。
- FISTTP:把第1个FPU寄存器的值转换为整数(使用舍入)并且从FPU堆栈弹出它。
- LDDQU:快速地从内存加载128位不对准的数据值
- MOVSHDUP:传送128位值,复制第2个和第4个32位数据元素
- MOVSLDUP:传送128位值,复制第1个和第3个32位数据元素
- MOVDDUP:传送64位值,复制值,使之称为128位值
- ADDSUBPS:对于打包单精度浮点值,对第2个和第4个32位值执行加法操作,对第1个和第3个32位值执行减法操作
- ADDSUBPD:对于打包双精度浮点值,对第2对64位值执行加法操作,对第1对执行减法操作
- HADDPS:对操作数的相邻的数据元素执行单精度浮点加法操作
- HADDPD:对操作数的相邻的数据元素执行双精度浮点加法操作
- HSUBPS:对操作数的相邻的数据元素执行单精度浮点减法操作
- HSUBPD:对操作数的相邻的数据元素执行双精度浮点减法操作