实验7 程序优化
评价一个程序的优劣,执行效率是一个重要因素。尤其是在汇编编程中,更应该体现汇编语言执行效率高的优势。执行效率从两个方面来衡量:程序在多长的时间内能够完成(运行时间);程序需要多大的存储空间(占用空间)。
7.1 指令代码的优化
完成同样一个功能,可以由选择不同的指令来完成。它们的执行效率是有所区别的,在对程序进行优化时,应该选取那些占用空间少、执行速度快的指令。
1. 选择执行速度快的指令
(1)寄存器清零
将寄存器清零,有以下几种指令:
mov eax, 0
sub eax, eax
xor eax, eax
SUB、XOR指令执行速度比MOV指令快,而且所需程序空间少。MOV指令需5字节,而SUB、XOR指令只需2字节。
00401277 B8 00 00 00 00 mov eax,0
0040127C 2B C0 sub eax,eax
0040127E 33 C0 xor eax,eax
所以,应该使用SUB、XOR指令。在清零时,XOR指令更常见一些。
(2)加减
要使EBX=EAX-30,简单的做法是:
mov ebx, eax
sub ebx, 30
而使用LEA指令完成同样功能的方法为:
lea ebx, [eax-30]
(3)乘除
求EAX=EAX/16,可以用除法指令:
xor edx, edx
mov ebx, 16
div ebx
然而,可以用SHR指令达到同样的效果:
shr eax, 4
求EAX=EAX*8,可以用乘法指令:
mov ebx, 8
mul eax
同样,用SHL指令的效果更好:
shl eax, 3
求EAX=EBX*5,可以用乘法指令:
mov eax, 5
mul ebx
可以使用LEA指令达到同样的效果:
lea eax,[ebx+ebx*4]
2. 操作的转化
除法指令比乘法指令的速度慢。如果程序中的除法操作中,除数为一个常数,那么可以将除法转换为乘法来进行,以提高程序执行的速度。
以下分别是125÷25、424÷25、6553600÷10、655389999÷65538的例子。程序中使用乘法操作来替代除法,乘法得到的结果EDX,就是除法操作的商。
mov eax, 125
mov esi, 0A3D70A4H ; esi = (100000000H + 24) / 25
mul esi
; EDX = 5
mov eax, 424
mov esi, 0A3D70A4H ; esi = (100000000H + 24) / 25
mul esi
; EDX = 16
mov eax, 6553600
mov esi, 1999999AH ; esi = (100000000H + 9) / 10
mul esi
; EDX = 655360
mov eax, 655389999
mov esi, 0000FFFEH ; esi = (100000000H + 65537) / 65538
mul esi
; EDX = 10000
那么,这种方法的原理是什么?这里,设被除数为a,除数为b,商为c,余数为d,均为32位二进制数。即:
a÷b = c 余 d+d, a = bc
记L=232=100000000H,求出:M=(L+(b–1))÷b。则:
c = aM / L
设:
L÷b = e mod f, L = be+f
分两种情况:
(1) f=0,即L能被b整除, M=(L+(b–1))÷b=L/b=e
(2) 0<f<b,L不能被b整除, M=(L+(b–1))÷b=(L/b)+1=e+1
在第1种情况下:
aM = a(L/b) = (aL/b) = ((bc+d)L/b) = ((bcL+dL)/b)
= cL+(dL/b)
因为d是余数,所以d<b。故0 ≤ (dL/b) < L。可知,a乘以M后,结果是64位数,高32位数就是c,即EDX。低32位数为dL / b。
在第2种情况下:
aM = a(e+1) = (bc+d)(e+1) = bce+de+bc+d = c(be+f)-cf+de+bc+d = cL-cf+de+bc+d
= cL+de+(b-f)c+d
因为b>f,所以de+(b-f)c+d>0。de+(b-f)c+d<L。
可知,a乘以M后,结果是64位数,高32位数就是c,即EDX。低32位数为de+(b-f)c+d。
当然,这种方法的局限是只能求得除法操作的商。
3. 求两个数中的较小的数
求eax=min(eax, ebx)。通常这需要用比较和分支来实现。
cmp eax, ebx
jle little
mov eax, ebx
little:
然而,CPU在执行这些指令时,分支处理需要额外的开销。
设x和y是32位二进制数,有以下公式:
min(x, y) = x+(((y–x)>>31)&(y-x))
如果y≥x,(y–x)的最高位(符号位)为0,(y–x)>>31=0。min(x, y)=x+(0&(y–x))=x。如果y<x,(y–x)的最高位(符号位)为1,(y–x)>>31=0xFFFFFFF。min(x, y)=x+(y–x)=y。
程序指令为:
sub ebx, eax ; ebx = y-x
mov ecx, ebx ; ecx = y-x
sar ecx, 31 ; ecx = (y-x)>>31
and ebx, ecx ; ebx = ((y-x)>>31)&(y-x)
add eax, ebx ; eax = x+(((y-x)>>31)&(y-x))
尽管程序在在优化后的长度上有所增加,由于避免了分支判断,CPU指令流水线的效率得到了充分的利用,在执行时间上大大缩短。
可以尝试依照上式写出max(x, y)的计算公式和程序指令。
4. 算法的优化
素数是只能被1和它自己整除的正整数。输入一个数后,要求找到小于这个数的最大素数,以及小于这个数的所有素数的个数。
对某一个数guess,要判断它是否为素数,简单的做法是将2到guess-1中每一个数作为除数,去除guess。如果商为0,则guess不是素数。
输入的数为limit,则guess可能的范围是2到limit。因此,程序是一个两层的循环。这里,guess从5开始,前面有2、3两个素数,nums=2。
nums = 2;
guess = 5;
while ( guess <= limit ) {
for (factor = 2; factor < guess; factor ++)
if (guess % factor == 0)
break;
if (factor > guess ) {
nums++;
maxprime = guess;
}
guess ++;
}
因为偶数不可能是素数,所以在外层循环中,guess可以每次加2,而不取偶数值。在内层循环中,factor也可以不取偶数。优化后的程序为:
nums = 2;
guess = 5;
while ( guess <= limit ) {
for (factor = 3; factor < guess; factor += 2)
if (guess % factor == 0)
break;
if (factor > guess ) {
nums++;
maxprime = guess;
}
guess += 2;
}
由此,得到了优化后的程序prime.asm。
;程序清单:prime.asm(查找素数)
.386
.model flat
option casemap:none
includelib msvcrt.lib
printf PROTO C format:ptr sbyte,:vararg
scanf PROTO C :dword,:vararg
.data
szInputFmtStr byte "%u", 0
Message0 byte "Find primes up to: ", 0
Message1 byte "Prime numbers between (1~%d): %d", 0ah, 0
Message2 byte "The maximum prime number is : %d", 0ah, 0
limit dword ? ; find primes up to this limit
guess dword ? ; the current guess for prime
nums dword 0
maxprime dword 0
.code
main proc C
invoke printf, offset Message0
invoke scanf, offset szInputFmtStr, offset limit
mov eax, limit
cmp eax, 5
jbe skip
mov nums, 2
mov eax, 5
mov guess, eax ; guess = 5;
L10: ; while ( guess <= limit )
mov eax, guess
cmp eax, limit
ja L50 ; use ja for unsigned numbers
mov ebx, 3 ; ebx is factor = 3;
L20:
cmp ebx, guess
jae L30 ; ebx > guess, guess is a prime
mov eax, guess
xor edx, edx
div ebx ; edx = edx:eax % ebx
cmp edx, 0
je L40 ; if !(guess % factor != 0)
add ebx, 2 ; factor += 2;
jmp L20
L30:
push guess
pop maxprime
inc nums
L40:
add guess, 2 ; guess += 2
jmp L10
L50:
invoke printf, offset Message1, limit, nums
invoke printf, offset Message2, maxprime
skip:
ret
main endp
end
继续进行分析,发现factor没有必要从3到guess取值,只要从3循环到n,n*n > guess即可。这样,这个循环的执行次数就会减少。例如:guess=81时,factor只要从3循环到9就可以了,共4次。而如果从3循环到81,则执行了39次(每次guess加2)。
将上面程序中的两行:
cmp ebx, guess
jae L30
修改为:
mov eax, ebx
mul eax
cmp edx, 0
ja L30
cmp eax, guess
ja L30
ebx是factor。将内层循环的终止条件从factor≥guess修改为factor*factor>guess。
修改过的程序增加了乘法操作,但是,它减少了内层循环的次数。修改后,程序的执行速度将大大加快。
5. 提高Cache命中率
程序对内存的访问有两个局部性特点:时间局部性和空间局部性。时间局部性是指访问某一个内存单元后,程序在以后的运行过程中很有可能再次访问这个单元,比如,程序在每次循环中都要访问某个变量。空间局部性是指在访问某一个内存单元后,程序在以后的运行过程中很有可能再次访问与这个单元相邻的其他单元,比如对数组的处理,访问第1个元素后,又会访问第2个、第3个元素等,而这些元素在内存中是顺序存放的。
基于时间局部性和空间局部性,计算机把最近被访问的内存及其相邻单元保留在Cache中,访问Cache中的数据比访问内存要快。当然,Cache的容量是有限的,在Cache填满后,新的数据访问操作会覆盖某些Cache。
在二维数组A[m][n]中,元素的存放顺序如图7-1所示。
A[0][0]
|
A[0][1]
|
…
|
A[0][n-1]
|
A[1][0]
|
A[1][1]
|
…
|
A[1][n-1]
|
A[2][0]
|
A[2][1]
|
…
|
A[2][n-1]
|
…
|
…
|
A[m-1][n-1]
|
图7-1
二维数组中元素在内存中的存放顺序
在处理二维数组时,一般需要两重循环。应该将行下标的变化(递增或递减)设计为外层循环,将列下标的变化(递增或递减)设计为内层循环。
for (i = 0; i < m; i++)
for (j = 0; j < n; j++)
A[i][j]++;
这样,在内层循环的处理过程中,访问的数据都是相邻的内存单元:A[i][0]、A[i][1]、A[i][2] ……A[i][n-1],空间局部性最优。Cache命中率高,程序的执行速度快。
如果将行下标的变化(递增或递减)设计为内层循环,则程序的执行速度就慢。
for (i = 0; i < n; i++)
for (j = 0; j < m; j++)
A[j][i]++;
在内层循环的过程中,元素的访问顺序是A[0][i]、A[1][i]、A[2][i] ……A[m-1][i]。空间局部性差,每一次访问都导致相应的元素所在的相邻单元被装入Cache,但这些单元并没有被立即访问,Cache命中率低。
6. 查表法
将十六进制数字0~15转换为’0’~’9’、’A’~’F’。常见的做法是:
add al, ‘0’
cmp al, ‘9’
jbe isdigit
add al, ‘A’-‘9’-1
isdigit:
如果al的初值为0~9,jbe指令会发生跳转,得到’0’~’9’。 如果al的初值为10~15,则jbe指令不会跳转,得到’A’~’F’。
HexChars byte ‘0123456789ABCDEF’
lea ebx, HexChars
xlat
XLAT指令的作用是将EBX的值加上AL,得到一个内存单元地址。从该地址中取出一个字节,送到AL中。
其结果就将AL中的0~15转换为’0’~’9’、’A’~’F’。这个转换过程比前面的直接转换要快,但它占用了16个字节的数据空间。
7.2 空间优化处理
1. 选用长度短的指令
在函数中,如果局部变量占4字节,进入函数时一般使用这条指令在堆栈中为局部变量分配空间:
sub esp, 4
而C编译器经常采用下面的指令,其效果也是ESP减去4:
push ecx
注意,“push ecx”占用的程序空间更少。
0040127C 83 EC 04 sub esp,4
0040127F 51 push ecx
2. 联合
某些情况下,程序中可能需要几个缓冲区,但同一时刻只会用到一个。例如,程序需要从文件中读出一部分数据,需要一个大小为4096字节的缓冲区。读入数据并对其处理完毕后,又需要构造一个输出字符串,长度为2000字节。
可以用这样的方式定义:
fileBuffer byte 4096 dup (?)
outputBuffer byte 2000 dup (?)
这样,在数据区中就需要4096+2000=6096字节。
可以将这两个缓冲区声明为一个联合。联合的用法和结构相似,其关键字为UNION。如:
unionBuf union
fileBuffer byte 4096 dup (?)
outputBuffer byte 2000 dup (?)
unionBuf ends
声明一个联合unionBuf,只是说明了unionBuf这样一个联合类型,并没有在数据区中给它分配空间。
接下来,在数据区中为联合分配空间。定义一个unionBuf类型的变量MyBuffer:
MyBuffer unionBuf <>
程序中,可以用点号“.”来指明MyBuffer的某个成员,例如:
lea esi, MyBuffer.fileBuffer
lea esi, MyBuffer.outputBuffer
利用联合可以节省数据区占用空间,这里只用了4096字节的空间。但是,这两个缓冲区的地址相同,所以这两个缓冲区不能同时使用。
3. 压缩存储
年、月、日组成一个日期,声明为一个结构,一共占4个字节。
oneday struc ;声明结构oneday
year word 0 ;年
month byte 0 ;月
day byte 0 ;日
oneday ends
这里,年的表示范围是0~65535,月、日的范围是0~255。实际上根本不会用到这么大的范围,数据的许多位都被浪费了。
假定年的表示范围是1900~2027,将年份减去1900后,范围是0~127。用7位二进制数就能表示出年,用4位二进制数能表示出月(范围是1~12),用5位二进制数能表示出日(范围是1~31)。这样,用7+4+5=16个二进制位就能表示出一个日期,只用两个字节。
这种用二进制位来表示成员的方法在汇编中被称做record。它的声明为:
fday record year:7, month:4, day:5
定义时可赋初值,令其year=87,month=7,day=1。
day2 fday <1987-1900, 7, 1>
在数据区中,day2相当于一个字,内容为0AEE1H。如图7-2所示。
15
|
14
|
13
|
12
|
11
|
10
|
9
|
8
|
7
|
6
|
5
|
4
|
3
|
2
|
1
|
0
|
1
|
0
|
1
|
0
|
1
|
1
|
1
|
0
|
1
|
1
|
1
|
0
|
0
|
0
|
0
|
1
|
年:
1010111B = 87
|
月:
0111B = 7
|
日:
00001B = 1
|
图7-2 record
的3个成员
record中的成员,从左至右安排。年占7位,是第15~9位;月占4位,是第8~5位;日占5位,是第4~0位。
MASK伪操作符返回一个成员的掩码,即该成员所在的二进制位全为1,而其他二进制位全为0。在这个例子中,各成员的掩码为:
MASK day2.year = 1111111000000000b;
MASK day2.month = 0000000111100000b;
MASK day2.day = 0000000000011111b;
WIDTH伪操作符返回该成员的位数。在这个例子中,各成员的位数为:
WIDTH day2.year = 7;
WIDTH day2.month = 4;
WIDTH day2.day = 5;
在程序中,可使用以下指令取出月份。
mov ax, day2
and ax, MASK day2.month
shr ax, WIDTH day2.day
首先,取出AX=0AEE1H,接着,将其他位清零,只保留月所在的位。最后,再将AX右移5位。注意,这里使用的是WIDTH day2.day,而不能使用WIDTH day2.month!
在程序中,可使用以下指令设置年份:
and day2, not MASK day2.year
mov ax, 2002-1900
shl ax, WIDTH day2.month+WIDTH day2.day
or day2, ax
首先,将年所在的位全部清零(not MASK day2.year等于01FFH)。接下来,令AX=102。将AX左移9位。
从上面的例子可以看出,节省数据空间的代价是程序代码变得复杂了。
7.3 MMX指令
MMX(Multimedia Extensions)是一套多媒体增强指令集,它使用单指令多数据技术(Single Instruction Multiple Data,SIMD)技术,以并行方式同时处理多个数据元素,从而提高了多媒体和通讯软件的运行速度。MMX指令集增加了57条新指令和一个64位数据类型QWORD(占8个字节)。
一个MMX指令可一次操作QWORD,即8个字节,这8个字节可以包括8个8位数据、4个16位数据或者2个32位数据。要求这些数据元素必须配对整齐。
一共有8个MMX寄存器,从mm0到mm7,每个寄存器64位。这64位是“借用”浮点寄存器的低64位,所以MMX指令执行后要用EMMS指令将寄存器清空。
1. 拷贝指令
MOVQ:64位数据拷贝,在2个MMX寄存器之间,或者在MMX寄存器和内存变量之间复制数据。
MOVD:32位数据拷贝,在MMX寄存器的低32位和内存变量之间复制数据。如果从内存向MMX寄存器拷贝,MMX高32位清零。
2. 分组指令
(1)PUNPCKLBW/PUNPCKLWD/PUNPCKLDQ
将SRC和DEST操作数的低32位交错组合,构成一个64位数据。PUNPCKLBW指令将低32位按照8位组合,如图7-3所示;PUNPCKLWD按照16位组合;PUNPCKLWD按照32位组合。
图7-3 PUNPCKLBW执行效果
(2)PUNPCKHBW/PUNPCKHWD/PUNPCKHDQ
将SRC和DEST操作数的高32位交错组合,构成一个64位数据。PUNPCKHBW指令将低32位按照8位组合,如图7-4所示;PUNPCKHWD按照16位组合;PUNPCKHWD按照32位组合。
图7-4 PUNPCKHBW执行效果
(3)PACKSSWB/PACKSSDW
PACKSSWB将SRC和DEST操作数中的8个16位带符号数,压缩成8个8位带符号数保存在DEST操作数中。PACKSSDW将SRC和DEST操作数中的4个32位带符号数,压缩成4个16位带符号数保存在DEST操作数中,如图7-5所示。
图7-5 PACKSSDW执行效果
(4)PACKUSWB
PACKUSWB将SRC和DEST操作数中的8个16位无符号数,压缩成8个8位无符号数保存在DEST操作数中。
3. 运算指令
(1)PADDB/PADDW/PADDD
PADDB将SRC和DEST操作数中的8个8位操作数相加;PADDW按16位相加;PADDD按32位相加。
(2)PADDSB/PADDSW、PADDUSB/ PADDUSW
与PADDB/PADDW相似,但是8位、16位带符号数的结果带有越界保护。例如,8位加法中,80+90=170,超过8位带符号数的最大值127,结果为127。
PADDUSB/ PADDUSW是带越界保护的无符号加法操作。例如,8位加法中,180+90=270,超过8位无符号数的最大值255,结果为255。
(3)PSUBB/PSUBW/PSUBD
按照8位、16位、32位操作数做减法。
(4)PSUBSB/PSUBSW、PSUBUSB/PSUBUSW
带越界保护的带符号数减法、无符号数减法。
(5)PMULLW/PMULHW
PMULLW将SRC和DEST操作数中的4个16位操作数相乘,保存4个积的低16位,如图7-6所示;PMULHW保存4个积的高16位。
图7-6 PMULLU执行效果
(6)PMADDWD
PMADDWD将SRC和DEST操作数中的4个16位操作数相乘,其中的2个积分别相加,作为32位数保存,如图7-7所示。
图7-7 PMADDWD执行效果
4. 逻辑指令
(1)PSRAW/PSRAD
PSRAW将DEST操作数中的4个16位操作数算术右移COUNT位,如图7-8所示。PSRAD按2个32位操作数执行算术右移。
图7-8 PSRAW执行效果
(2)PSRLW/PSRLD/PSRLQ
与PSRAW/PSRAD 相似,PSRLW/PSRLD完成逻辑右移运算。PSRLQ完成64位操作数的逻辑右移运算。
(3)PSLLW/PSLLD
PSLLW/PSLLD完成16位操作数、32位操作数的左移运算。
(4)PXOR
SRC和DEST操作数完成64位异或运算,结果保存在DEST中。
以下示例程序计算2个向量的内积。idot_product_char_c是算法的C语言实现,idot_product_char_mmx采用嵌入式汇编指令,用MMX指令完成。向量中有n个元素时,第1种方法需要执行n次乘法、n次加法;而采用MMX只需要执行n/4次乘法、n/4次加法。
;程序清单:idot.c(向量内积的计算)
#include <stdlib.h>
signed char a[16] = {12, 24, 30, -12, 24, 0, -70, 123, -4, 19, 73, -80, 23, -69, -20, 45};
signed char b[16] = {70, 68, -79, 23, -6, 43, 39, -27, 100, -26, 36, 61, 0, 18, -105, 82};
int idot_product_char_c(signed char *a, signed char *b, int n)
{
int r = 0;
int i;
for (i = 0 ; i < n ; i++)
{
r = r + a[i] * b[i];
}
return r;
}
int idot_product_char_mmx(signed char *a, signed char *b, int n)
{
int r;
int i;
_asm {
pxor mm0,mm0 ; mm0清0
}
for (i = 0; i < n; i+=8) {
_asm {
mov ebx,a
mov ecx,b
add ebx,i
add ecx,i
movq mm1,qword ptr [ebx] ; mm1=a[7..0]
movq mm2,qword ptr [ecx] ; mm2=b[7..0]
punpcklbw mm6,mm1 ; mm6=a[3],0,a[2],0,a[1],0,a[0],0
punpcklbw mm7,mm2 ; mm7=b[3],0,b[2],0,b[1],0,b[0],0
psraw mm6,8 ; mm6=0,a[3],0,a[2],0,a[1],0,a[0]
psraw mm7,8 ; mm7=0,b[3],0,b[2],0,b[1],0,b[0]
punpckhbw mm4,mm1 ; mm4=a[7],0,a[6],0,a[5],0,a[4],0
punpckhbw mm5,mm2 ; mm5=b[7],0,b[6],0,b[5],0,b[4],0
psraw mm4,8 ; mm4=0,a[7],0,a[6],0,a[5],0,a[4]
psraw mm5,8 ; mm5=0,b[7],0,b[6],0,b[5],0,b[4]
pmaddwd mm6,mm7 ; mm6=a[3]*b[3],a[2]*b[2],a[1]*b[1],a[0]*b0
pmaddwd mm4,mm5 ; mm4=a[7]*b[7],a[6]*b[6],a[5]*b[5],a[4]*b4
paddd mm0,mm6 ; mm0+=a[3]*b[3],a[2]*b[2],a[1]*b[1],a[0]*b0
paddd mm0,mm4 ; mm0+=a[7]*b[7],a[6]*b[6],a[5]*b[5],a[4]*b4
}
}
_asm {
movq mm1,mm0
psrlq mm1,32 ; mm1 = mm0 右移32位
paddd mm0,mm1 ; mm0和mm1的高32位、低32位分别相加
movd r,mm0 ; 取mm0的低32位到r
emms
}
return r;
}
int main()
{
int r1,r2;
r1 = idot_product_char_c(a,b,16);
r2 = idot_product_char_mmx(a,b,16);
printf("idot_product_char_c() = %d/n", r1);
printf("idot_product_char_mmx() = %d/n", r2);
}
7.4 SSE指令
SSE(Streaming SIMD Extensions)是Pentium III处理器的扩展指令集。与MMX相似,这些扩展指令集具有SIMD能力,在一个CPU指令周期同时处理多个数据。
然而,MMX的SIMD只限于整数处理,而SSE指令可以完成单精度浮点数的并行处理,可以同时对4个32位的浮点数操作。
MMX和SSE的另一重要区别是,MMX并没有定义新的寄存器,mm0~mm7的大小为64位,占用CPU的80位浮点寄存器的低64位;SSE定义了8个128位寄存器xmm0~xmm7,不再占用浮点寄存器。每个128位寄存器可以同时存放4个单精度浮点数。
Pentium III增加了70条新的SSE指令。按照指令的操作数划分,可分为操作包裹数据(packed data)的指令和操作标量数据(scalar data)的指令。包裹指令都带有ps前缀,同时操作128位数据中的4个元素;而标量指令有一个ss前缀,只操作这个128位数据中的最后一个元素。
1. 传送指令
(1)MOVAPS/MOVUPS
128位数据拷贝,在2个XMM寄存器之间,或者在XMM寄存器和内存变量之间复制数据。MOVAPS要求内存变量按照16个字节对齐(地址的低4位全部为0);而MOVUPS对变量地址不做要求。
(2)MOVSS
MOVSS:32位数据拷贝,在XMM寄存器的低32位和内存变量之间复制数据。如果从内存向XMM寄存器拷贝,MMX高96位清零。
(4)CVTDQ2PD/CVTDQ2PS
CVTDQ2PD将SRC操作数中的2个双字(整数)转换为2个双精度浮点数,存入DEST操作数中。CVTDQ2PS将SRC操作数中的4个双字(整数)转换为4个单精度浮点数,存入DEST操作数中。
限于篇幅,其他的转换指令此处省略。
2. 分组指令
SSE的分组指令与MMX基本相同,指令后面指定XMM寄存器时,执行SSE的分组操作(128位);指令后面指定MMX寄存器时,执行64位分组操作。
(1)PUNPCKLBW/PUNPCKLWD/PUNPCKLDQ/PUNPCKLQDQ
将SRC和DEST操作数的低64位交错组合,构成一个128位数据。PUNPCKLBW指令将低64位按照8位组合;PUNPCKLWD按照16位组合;PUNPCKLWD按照32位组合;PUNPCKLQDQ按照64位组合。
(2)PUNPCKHBW/PUNPCKHWD/PUNPCKHDQ/PUNPCKHQDQ
将SRC和DEST操作数的高64位交错组合,构成一个128位数据。PUNPCKHBW指令将低64位按照8位组合;PUNPCKHWD按照16位组合;PUNPCKHWD按照32位组合;PUNPCKHQDQ按照64位组合。
(3)PACKSSWB/PACKSSDW
PACKSSWB将SRC和DEST操作数中的16个16位带符号数,压缩成16个8位带符号数保存在DEST操作数中。PACKSSDW将SRC和DEST操作数中的8个32位带符号数,压缩成8个16位带符号数保存在DEST操作数中。
(4)PACKUSWB
PACKUSWB将SRC和DEST操作数中的16个16位无符号数,压缩成16个8位无符号数保存在DEST操作数中。
3. 运算指令
(1)ADDPD/ADDPS/ADDSD/ADDSS
将SRC和DEST操作数中的浮点操作数相加,保存在DEST中。ADDPD完成2次双精度浮点数加法;ADDPS完成4次单精度浮点数加法;ADDSD完成1次双精度浮点数加法;ADDSS 完成1次单精度浮点数加法。
(2)SUBPD/SUBPS/SUBSD/SUBSS
减法SSE指令,完成2次双精度、4次单精度、1次双精度、1次单精度浮点数减法。
(3)MULPD/MULPS/MULSD/MULSS
乘法SSE指令,完成2次双精度、4次单精度、1次双精度、1次单精度浮点数乘法。
(4)DIVPD/DIVPS/DIVSD/DIVSS
除法SSE指令,完成2次双精度、4次单精度、1次双精度、1次单精度浮点数除法。
示例程序计算256次单精度浮点数乘法,只需要执行64次MULPS指令,每次可完成4次乘法操作。
;程序清单: floatx.asm(SSE单精度浮点数乘法)
.686 ; 必须写成686才可以支持SSE
.xmm ; 使用XMM寄存器
.model flat,stdcall
option casemap:none
includelib msvcrt.lib
printf PROTO C format:ptr sbyte,:vararg
.data
align 16 ; 按照16字节对齐
aArray real4 100h dup(2.0) ; 第1个数组,共256个元素, 单精度格式
bArray real4 100h dup(3.0) ; 第2个数组,共256个元素, 单精度格式
cArray real4 100h dup(0.0) ; 结果, 单精度格式, 每个元素占4字节
result real8 0.0 ; 双精度浮点数, 占8字节
nIndex equ 20
szFmt byte 'cArray[%d]=%8.3f',0ah,0
.code
start:
lea esi,aArray ; esi指向第1个数组
lea edi,bArray ; edi指向第2个数组
lea edx,cArray ; edx指向第3个数组
mov ecx,100h/4 ; 循环64次,每次可计算4个元素
Calx:
movaps XMM0,[esi] ; 装入第1个数组的4个元素到XMM0
movaps XMM1,[edi] ; 装入第2个数组的4个元素到XMM1
mulps XMM0,XMM1 ; XMM0、XMM1中的4个元素分别相乘
movaps [edx],XMM0 ; 保存结果到cArray中
add edi,16 ; 指向aArray的下一组元素
add esi,16 ; 指向bArray的下一组元素
add edx,16 ; 指向cArray的下一组元素
loop Calx
mov esi,nIndex ; nIndex=10
FLD cArray[esi*4] ; 将cArray[20]转换为双精度浮点数
FSTP result ; 双精度浮点数保存在result中
invoke printf,offset szFmt,esi,result
ret
end start
7.5 实验题:图像像素反转优化
利用MMX指令优化图像处理。参考ProcessBmp_C函数,将每一个像素值求反,编写与之等价的MMX指令序列。MMX汇编指令可用inline汇编的形式包含在C程序中。图片的大小为544x400像素。
要求:
1.
将图片作为程序的资源,调用LoadBitmap()、GetBitmapBits()函数获得包含像素值的内存块;进行求反处理后,调用SetBitmapBits()函数将包含处理过的像素的内存块复制到HBITMAP所代表的图片上。
2.
调用Windows GDI函数显示出三种图片:未经处理的图片、ProcessBmp_C函数处理过的图片、MMX汇编程序处理过的图片;
3.
计算出C函数、MMX处理所耗费的时间,可使用QueryPerformanceFrequency()、QueryPerformanceCounter(),应精确到微秒级。
4.
使用MMX指令优化后,效率提高了多少倍?分析其原因。
void ProcessBmp_C()
{
int i;
unsigned char *p,*q;
p = bBuffer;
q = cBuffer;
for (i = 0; i < nBitmapSize; i++)
{
*q++ = 0xff-(*p++);
}
}