优化汇编例程(10)

10. 优化大小

代码缓存可以保存8到32kb代码,如第77页,第11章解释。如果将代码关键部分保持在代码缓存里有问题,那么可以考虑减小代码的大小。减小代码大小还可以改进代码的解码。在Core2处理器上,不超过64字节代码的循环执行得特别快。

如果速度不重要,甚至可能希望以降低速度为代价,减小代码大小。

32位代码通常大于16位代码,因为在32位代码中地址与数据常量需要4字节,在16位代码需要2字节。不过,16位代码有其他损失,特别是因为段前缀。64位代码无需对地址使用比32位代码更多的字节,因为它可以使用32位RIP相对地址。64位代码比32位代码稍大,因为REX前缀与其他微小的差别,但它也可能比32位代码小,因为寄存器数量的增加减少了内存变量的需要。

10.1. 选择更短的指令

某些指令有短形式。带有整数寄存器的PUSH与POP指令仅需1字节。XCHG EAX, reg32也是一条单字节指令,比MOV指令占据更少空间,但XCHG比MOV慢。在32位模式中带有32位寄存器,或者16位模式中带有16位寄存器的INC与DEC仅需1字节。在64位模式中,INC与DEC段形式不可用。

以下指令在使用累加器时,比使用寄存器,要少1字节:带有一个没有符号展开立即数的ADD,ADC,SUB,SBB,AND,OR,XOR,CMP,TEST。这也适用于在16位与32位模式里,带有一个内存操作数且没有指针寄存器的MOV指令,但64位模式中不适用。例子:

; Example 10.1. Instruction sizes

add eax, 1000 小于 add ebx, 1000

mov eax, [mem] 小于 mov ebx, [mem],除了在62位模式中。

带有指针的指令,在仅有一个基址指针(除了ESP,RSP或R12)与一个位移时,比具有一个比例索引寄存器,或者同时有基址指针与索引寄存器,或者ESP,RSP或R12作为基址指针时,要少一个字节。例子:

; Example 10.2. Instruction sizes

mov eax, array[ebx] 小于 mov eax, array[ebx*4]

mov eax, [ebp+12] 小于 mov eax, [esp+12]

使用BP,EBP,RBP或R13作为基址指针、没有位移、没有索引的指令比使用其他寄存器要多1字节:

; Example 10.3. Instruction sizes

mov eax, [ebx] 小于 mov eax, [ebp],但

mov eax, [ebx+4] 大小等于 mov eax, [ebp+4].

带有比例索引指针、没有基址指针的指令必须有4字节位移,即使它是0:

; Example 10.4. Instruction sizes

lea eax, [ebx+ebx] 短于 lea eax, [ebx*2].

如果使用了R8 ~ R15或XMM8 ~ XMM15中至少一个寄存器,64位模式中的指令需要一个REX前缀。因此,使用这些寄存器的指令比使用其他寄存器的指令要长1字节,除非出于其他原因需要一个REX前缀:

; Example 10.5a. Instruction sizes (64 bit mode)

mov eax, [rbx] 小于 mov eax, [r8].

; Example 10.5b. Instruction sizes (64 bit mode)

mov rax, [rbx] 大小等于 mov rax, [r8].

在例子10.5a中,通过使用寄存器RBX,而不是R8,作为指针,可以避免REX前缀。但在例子10.5b中,对64位操作数大小,无论如何需要一个REX前缀,且指令不能有超过一个REX前缀。

浮点计算可以通过使用浮点栈寄存器ST(0) ~ ST(7)的旧式x87形式指令,或者使用XMM寄存器的新式SSE形式指令来完成。X87形式指令比后者更紧凑,例如:

; Example 10.6. Floating point instruction sizes

fadd st(0), st(1) ; 2 bytes

addsd xmm0, xmm1 ; 4 bytes

x87形式代码可能更有优势,即使它要求额外的FXCH指令。在当前处理器上,这两类浮点指令的执行速度没有太大区别。不过,在未来处理器上x87指令被视为过时,效率下降,是可能的。

支持AVX指令集的处理器可以两个不同方式编码XMM指令,使用VEX前缀或使用旧的前缀。有时VEX版本更短,有时旧版本更短。不过,混用没有VEX前缀的XMM指令与使用YMM寄存器的指令,在某些处理器上有严重的性能损失。

AVX-512指令集使用一个称为EVEX的4字节新前缀。尽管EVEX前缀比VEX前缀长1到2个字节,它允许使用指针与偏移的内存操作数更高效的编码。在使用EVEX前缀时。带有4字节偏移的内存操作数,有时可被1字节比例偏移所替换。因而总指令长度更小。

10.2. ​​​​​​​使用更短的常量与地址

许多跳转地址、数据地址与地址常量可以表示为符号展开的8位常量。这节省了大量的空间。符号扩展字节仅能用于这个值在-128到127之间时。

至于跳转地址,这意味着段跳转需要2字节代码,而超过127字节的跳转,如果无条件需要4字节,有条件6字节。

类似的,如果可以表示为一个指针与-128与127之间的位移,数据地址需要更少空间。下面的例子假定[mem1]与[mem2]都是数据段中的静态内存地址,它们之间的距离小于128字节:

; Example 10.7a, Static memory operands

mov ebx, [mem1]                  ; 6 bytes

add ebx, [mem2]                   ; 6 bytes

简化为:

; Example 10.7b, Replace addresses by pointer

mov eax, offset mem1                        ; 5 bytes

mov ebx, [eax]                                      ; 2 bytes

add ebx, [eax] + (mem2 - mem1)      ; 3 bytes

在64位模式中,需要将mov eax, offset mem1替换为多1字节的lea rax, [mem1]。如果可以使用同一个指针许多次,使用指针的好处明显增加。如果使用静态内存位置与绝对地址,在栈上保存数据,并使用ESP或ESP作为指针,将使代码更小,当然数据必须在指针的+/-127范围内。使用PUSH及POP读写临时寄存器数据甚至更短。

数据常量,如果在-128到127之间,也可能需要更少空间。大多数带有立即数的指令,在操作数是符号扩展的单个字节时,有一个短形式。例子:

; Example 10.8, Sign-extended operands

push 200                                  ; 5 bytes

push 100                                  ; 2 bytes, sign extended

add ebx, 128                           ; 6 bytes

sub ebx, -128                          ; 3 bytes, sign extended

带有立即数、使用符号扩展8位常量没有短形式的指令,仅有MOV、TEST、CALL与RET。带有32位立即数的TEST指令可有各种更短的替代方案,依赖于每个个案的逻辑。一些例子:

; Example 10.9, Alternatives to test with 32-bit constant

test eax, 8                                  ; 5 bytes

test ebx, 8                                  ; 6 bytes

test al, 8                                      ; 2 bytes

test bl, 8                                      ; 3 bytes

and ebx, 8                                   ; 3 bytes

bt ebx, 3                                      ; 4 bytes (uses carry flag)

cmp ebx, 8                                  ; 3 bytes

在32位与64位模式中,不建议使用带有16位处理的版本,比如TEST AX, 800H,因为在某些处理器上,长度改变前缀解码将导致性能损失,如手册3《Intel,AMD与VIA CPU微架构》所解释。

MOV register, constant更短的替代通常是有用的。例子:

; Example 10.10, Loading constants into 32-bit registers

mov eax, 0                                   ; 5 bytes

sub eax, eax                                 ; 2 bytes

mov eax, 1                                    ; 5 bytes

sub eax, eax / inc eax                  ; 3 bytes

push 1 / pop eax                          ; 3 bytes

mov eax, -1                                   ; 5 bytes

or eax, -1                                       ; 3 bytes

也可以考虑减小静态数据的大小。显然,通过对元素使用更小的数据,可以减小一个数组。例如,如果确定数据可以放入更小的空间,使用16位整数,而不是32位整数。访问16位整数的代码略大于访问32位整数的代码,但对大数组来说,数据的减小足以抵消代码的增加。在32位及64位模式里,应该避免带有16位立即数的指令,因为长度改变前缀解码的问题。

10.3. ​​​​​​​重用常量

如果相同的地址或常量被使用多次,可以把它载入一个寄存器。带有4字节立即数的MOV,如果在MOV之前,该寄存器的值已知, 有时可以被一个算术指令替换。例子:

; Example 10.11a, Loading 32-bit constants

mov [mem1], 200                             ; 10 bytes

mov [mem2], 201                             ; 10 bytes

mov eax, 100                                     ; 5 bytes

mov ebx, 150                                     ; 5 bytes

替换为:

; Example 10.11b, Reuse constants

mov eax, 200                                     ; 5 bytes

mov [mem1], eax                              ; 5 bytes

inc eax                                                 ; 1 byte

mov [mem2], eax                              ; 5 bytes

sub eax, 101                                       ; 3 bytes

lea ebx, [eax+50]                               ; 3 bytes

10.4. ​​​​​​​64位模式中的常量

在64位模式中,有几个方法将常量移进一个64位寄存器:使用一个64位处理,使用一个32位符号扩展处理,及使用一个32位零扩展常量:

; Example 10.12, Loading constants into 64-bit registers

mov rax, 123456789abcdef0h         ; 10 bytes (64-bit constant)

mov rax, -100                                      ; 7 bytes (32-bit sign-extended)

mov eax, 100                                       ; 5 bytes (32-bit zero-extended)

即使常量能放入一个零扩展处理,某些汇编器使用符号扩展版本,而不是更短的零扩展版本。可以通过说明目标寄存器,强制汇编器使用零扩展版本。写入一个32位寄存器总是零扩展为64位寄存器。

10.5. ​​​​​​​64位模式中的地址与指针

对地址中的基址与索引,64位代码倾向使用64位寄存器,对其他,使用32位寄存器。例子:

; Example 10.13, 64-bit versus 32-bit registers

mov eax, [rbx + 4*rcx]

inc rcx

这里,可以将inc rcx改为inc ecx,节省一个字节。这能奏效,因为索引寄存器中的值一定小于232。不过,在某些系统里,基址指针可能超过232,因此不能将add rbx, 4替换为add ebx, 4。在64位模式中,在中括号内永远不要把32位寄存器用作基址或索引。

在间接地址的中括号内使用64位寄存器,其他地方使用32位寄存器的规则,也适用于LEA指令。例子:

; Example 10.14. LEA in 64-bit mode

lea eax, [ebx + ecx]                  ; 4 bytes (needs address size prefix)

lea eax, [rbx + rcx]                   ; 3 bytes (no prefix)

lea rax, [ebx + ecx]                   ; 5 bytes (address size and REX prefix)

lea rax, [rbx + rcx]                    ; 4 bytes (needs REX prefix)

使用32位目标与64位地址的形式优先,除非需要一个64位结果。这个版本的执行时间不超过使用64位目标的版本。使用地址大小前缀的形式永远不要使用。

在64位程序中,通过使用相对于映像基址或某个引用点的32位指针,可以使一个64位指针数组变小。这以使这些指针的代码更大为代价,因为需要加上映像基址来减小这个指针数组。这是否有好处,取决于数组的大小。例子:

; Example 10.15a. Jump-table in 64-bit mode

.data

JumpTable DQ Label1, Label2, Label3, ..

.code

mov eax, [n]                                       ; Index

lea rdx, JumpTable                            ; Address of jump table

jmp qword ptr [rdx+rax*8]              ; Jump to JumpTable[n]

使用映像相对指针的实现仅在Windows中可行:

; Example 10.15b. Image-relative jump-table in 64-bit Windows

.data

JumpTable DD imagerel(Label1),imagerel(Label2),imagerel(Label3),..

extrn __ImageBase:byte

.code

mov eax, [n]                                                                ; Index

lea rdx, __ImageBase                                                 ; Image base

mov eax, [rdx+rax*4+imagerel(JumpTable)]         ; Load image rel. address

add rax, rdx                                                                  ; Add image base to address

jmp rax                                                                          ; Jump to computed address

Mac OS X的Gnu编译器使用跳转表本身作为一个引用点:

; Example 10.15c. Self-relative jump-table in 64-bit Mac

.data

JumpTable DD Label1-JumpTable, Label2-JumpTable, Label3-JumpTable

.code

mov eax, [n]                                                                    ; Index

lea rdx, JumpTable                                                         ; Table and reference point

movsxd rax, [rdx + rax*4]                                              ; Load address relative to table

add rax, rdx                                                                      ; Add table base to address

jmp rax                                                                              ; Jump to computed address

汇编器可能不能制作从数据段到代码段的自相对引用。

最短的替代方案是使用32位绝对指针。这个方法仅在能确定所有地址都小于231时使用:

; Example 10.15d. 32-bit absolute jump table in 64-bit Linux

; Requires that addresses < 2^31

.data

JumpTable DD Label1, Label2, Label3, .. ; 32-bit addresses

.code

mov eax, [n]                                                                  ; Index

mov eax, JumpTable[rax*4]                                       ; Load 32-bit address

jmp rax                                                                           ; Jump to zero-extended address

在例子10.15d中,JumpTable的地址是一个符号扩展到64位的32位可重定位地址。如果这个地址小于231,这奏效。Label1的地址等,是零扩展的,因此如果这些地址都小于232,这可以工作。例子10.15d的方法仅能在确定映像基址加上程序大小小于231时使用。这能在Linux与BSD、某些情形下的Windows中的应用程序里工作,但Mac OS X不行(参考第16页)。

甚至以相对于一个合适引用点的16位偏移替换64位或32位指针,也是可能的:

; Example 10.15d. 16-bit offsets to a reference point

.data

JumpTable DW 0, Label2-Label1, Label3-Label1, ..

.code

mov eax, [n]                                                           ; Index

lea rdx, JumpTable                                                ; Address of table (RIP-relative)

movsx rax, word ptr [rdx+rax*2]                        ; Sign-extend 16-bit offset

lea rdx, Label1                                                        ; Use Label1 as reference point

add rax, rdx                                                             ; Add offset to reference point

jmp rax                                                                    ; Jump to computed address

例子10.15d把Label1用作一个引用点。它仅在所有标签都在Label1∓ 215范围内工作。这个表包含符号扩展的16位偏移,并与引用点相加。

上面的例子展示了保存代码指针的各种方法。同样的方法可用于数据指针。指针可以保存为一个64位绝对地址、32位相对地址、32位绝对地址或相对于一个合适引用点的16位偏移。使用相对于映像基址或引用点的方法,仅在存在许多指针时,才值得额外的代码。这通常是大switch语句以及链接列表的情形。

10.6. ​​​​​​​出于对齐的目的,使指令更长

存在使用前面段落建议的反例,使指令变得更长,而取得优势的情形。最重要的情形是需要对齐的循环入口(参考第81页)。不是插入NOP来对齐循环入口标签,可以使前面的指令超过最小长度,使循环入口变得正确对齐。指令的更长版本不需要更多时间执行,因此可以节省执行NOP的时间。

汇编器通常将选择一条指令最短的可能形式。选择相同指令或等效指令的更长形式通常是可能的。这可以几个方式来做到。

使用一般形式而不是指令的段形式

INC,DEC,PUSH,POP,XCHG,ADD,MOV的短形式没有一个mod-r/m字节(参考第19页)。可以带一个mod-reg-r/m字节的一般形式编码相同的指令。例子:

; Example 10.16. Making instructions longer

inc eax                                                   ; short form. 1 byte (in 32-bit mode only)

DB 0FFH, 0C0H                                     ; long form of INC EAX, 2 bytes

push ebx                                                ; short form. 1 byte

DB 0FFH, 0F3H                                      ; long form of PUSH EBX, 2 bytes

使用更长的等效指令

例子:

; Example 10.17. Making instructions longer

inc eax                                                     ; 1 byte (in 32-bit mode only)

add eax, 1                                               ; 3 bytes replacement for INC EAX

mov eax, ebx                                          ; 2 bytes

lea eax, [ebx]                                          ; can be any length from 2 to 8 bytes, see below

使用4字节立即数

带有符号扩展8位立即数的指令可被带有32位立即数的版本替代:

; Example 10.18. Making instructions longer

add ebx, 1                                                 ; 3 bytes. Uses sign-extended 8-bit operand

add ebx, 9999                                          ; Use dummy constant too big for 8-bit operand

ORG $ - 4                                                   ; Go back 4 bytes (MASM syntax)

DD 1                                                           ; and overwrite the 9999 operand with a 1.

上面将使用6字节的码字编码ADD EBX, 1。

向指针添加0位移

在32位或64位模式中,带有指针的指令可以有1或4字节的位移(在16位模式中1或2字节)。可使用一个0的伪位移使指令变长:

; Example 10.19. Making instructions longer

mov eax, [ebx]                                             ; 2 bytes

mov eax, [ebx+1]                                         ; Add 1-byte displacement. Total length = 3

ORG $ - 1                                                       ; Go back one byte (MASM syntax)

DB 0                                                                ; and overwrite displacement with 0

mov eax, [ebx+9999]                                   ; Add 4-byte displacement. Total length = 6

ORG $ - 4                                                        ; Go back 4 bytes (MASM syntax)

DD 0                                                                ; and overwrite displacement with 0

同样可以使用LEA EAX, [EBX+0]替换MOV EAX, EBX。

使用SIB字节

带有内存操作数的指令可以有一个SIB字节(参考第19页)。一个SIB字节可以加到一条没有的指令上,使指令加长1字节。在16位模式,或64位RIP相对地址中,不能使用SIB字节。例子:

; Example 10.20. Making instructions longer

mov eax, [ebx]                                              ; Length = 2 bytes

DB 8BH, 04H, 23H                                        ; Same with SIB byte. Length = 3 bytes

DB 8BH, 44H, 23H, 00H                               ; With SIB byte and displacement. 4 bytes

使用前缀

一个使指令变长的容易的方法是添加不必要的前缀。所有带有内存操作数的指令可以有一个段前缀。DS段前缀很少需要,但可以添加它,而不改变指令的含义:

; Example 10.21. Making instructions longer

DB 3EH                                                           ; DS segment prefix

mov eax, [ebx]                                              ; prefix + instruction = 3 bytes

使用具有内存操作数的指令可以有一个段前缀,包括LEA。实际上向没有内存操作数的指令添加段前缀也是可能的。这样无意义的前缀只是被忽略。但没有绝对的保证,在未来的处理器上,无意义的前缀会没有含义。例如,P4在分支指令上使用段前缀作为分支预测暗示。要我说,在将来的处理器上,段前缀对内存操作数的指令,即带有mod-reg-r/m字节的指令,有任何不良作用,这个可能性很低。

在64位模式中,段前缀CS,DS,ES与SS没有作用,不过根据《AMD64 Architecture Programmer’s Manual, Volume 3: General-Purpose and System Instructions, 2003》,它们仍然允许。

在64位模式中,也可以使用一个空的REX前缀来加长指令:

; Example 10.22. Making instructions longer

DB 40H                                                                ; empty REX prefix

mov eax,[rbx]                                                     ; prefix + instruction = 3 bytes

在64位模式中,空REX前缀可安全地应用到几乎所有还没有REX前缀的指令,除了使用AH,BH,CH或DH的指令。REX前缀不能用在32位或16位模式里。REX前缀必须跟在其他前缀后面,指令不能有多个REX前缀。

AMD的优化手册建议最多使用3个操作数大小前缀(66H)作为填充。但这个前缀仅能用在不受这个前缀影响的指令上,即NOP与x87浮点指令。段前缀适用范围要更广且有相同的效果——或者没有影响。

向任何指令添加多个相同的前缀是可能的,只要指令整体长度不超过15。例如,一条指令可以有2或3个DS段前缀。不过在许多处理器上,具有多个前缀的指令需要额外时间解码。在AMD处理器上,不要使用超过3个前缀,包括任何必须的前缀。在Intel Core2及更新的处理器上,前缀数没有限制,只要指令整体长度不超过15字节,但较早的Intel处理器不能无性能损失地处理超过1或2个前缀。

使用地址大小前缀作为填充不是一个好主意,因为这可能减慢指令解码。

不要贴着跳转标签前面放置伪前缀来对齐它:

; Example 10.23. Wrong way of making instructions longer

L1: mov ecx, 1000

DB 3EH                                                                   ; DS segment prefix. Wrong!

L2: mov eax, [esi]                                                 ; Executed both with and without prefix

在这个例子中,当来自L1时,MOV EAX, [ESI]指令将带着一个DS段前缀解码,但当来自L2时没有。原则上这能工作,但某些微处理器记住指令边界在何处,当同一条指令在两个不同的地方开始时,这样的处理器会混乱。对此,可能有性能损失。

支持AVX指令集的处理器使用VEX前缀,这些前缀长度是2或3字节。一个2字节的VEX字节总可以被一个3字节VEX前缀替换。VEX前缀之前可以是段前缀,但不能是其他。VEX前缀后不允许其他前缀。大多数使用XMM或YMM寄存器的指令可以有一个VEX前缀。不要混用带有VEX前缀的YMM向量指令与没有VEX前缀的XMM指令(参考第13.1节)。少数较新的通用寄存器上的指令也使用VEX前缀。

AVX-512指令集使用名为EVEX的4字节前缀。通过以EVEX前缀替换VEX前缀,指令可以变长。

建议使用调试器或反汇编器检查手写指令,以确定它们是正确的。

10.7. ​​​​​​​为对齐使用多字节NOP

多字节NOP字节有操作码0F 1F + 一个伪内存操作数。可以通过向伪内存操作数添加1或4字节的位移与SIB字节,添加1或多个66H前缀,调整多字节NOP指令的长度。在较旧的微处理器上,过多的前缀会导致延迟,但在大多数处理器上,至少两个前缀是可接受的。最多10字节的NOP,可以不超过2个前缀,以这个方式构建。如果处理器可以处理多个前缀,没有性能损失,那么长度最多可以是15字节。

多字节NOP比常用的伪NOP,如MOV EAX, EAX或LEA RAX, [RAX+0],更高效。多字节NOP指令在所有Intel P6及更新的处理器族,以及AMD Athlon、K7及更新的处理器上支持,即几乎所有支持条件移动的处理器。

在Sandy Bridge处理器上,多字节NOP解码速度低于其他指令,包括带有前缀的单字节NOP。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值