汇编语言中子程序的优化

optimizing assembly 总结

术语

Intrinsic functions :机器指令的高层语言表示。

Call convention:关于函数调用的参数顺序,参数和返回值是用堆栈传递还是用寄存器传递,由函数还是由调用者清除堆栈的一些约定。

Name mangling:为了支持函数重载,向链接器提供关于函数的参数信息,通过将参数类型的代码附加到函数名中来完成的。

Shadow space:堆栈上为函数参数传递预留的一定字节的空间。阴影空间是指如果根据__cdecl规则在堆栈上传输前四个参数,则会存储它们的位置。阴影空间属于已调用的函数,该函数允许在阴影空间中存储参数(或任何其他内容)。

Inline assembly:在c++文件中可以直接使用汇编代码。

position-independent code:是指可在主存储器中任意位置正确地运行,而不受其绝对地址影响的一种机器码。代码部分不包含需要重定位的绝对地址,但只包含自相关地址地址 . 因此,代码部分可以加载到任意存储器地址并在多个进程之间共享 .数据部分不在多个进程之间共享,因为它通常包含可写数据 . 因此,数据部分可能包含需要重定位的指针或地址。

GOT:包含所有静态对象的地址。

Latency:指令开始执行到结束消耗时钟周期数。

Throughput:一个时钟周期内,同一指令执行的最大次数。

ABI:是关于如何调用函数、如何传递参数和返回值以及允许更改哪些注册函数的标准。

Red space:一个函数可以安全地将堆栈上方的数据存储在红色区域中,只要它不被任何PUSH或CALL指令所覆盖。

基础知识

1.常见的编程陷阱

1)在函数调用之前保存寄存器,在函数调用结束恢复寄存器。

2)函数中的push指令和pop指令数目相等。

3)不要使用有特殊用途的寄存器。

4)寻址堆栈是根据栈顶指针的位置进行寻址,每次push、pop都会修改栈顶指针,寻址时注意栈顶 指针是否改变。

5)指令对变量名有时解释为变量的地址,有时解释为变量的值。

6)如果想取消name mangling,用extern c。

7)汇编语言函数调用结束后返回需要RET和ENDP两条指令。

8)函数调用语句之前堆栈指针地址必须被16整除。

9)函数调用结束后,要清除浮点寄存器堆栈、清除MMX,YMM、ZMM寄存器、清除方向标志位。

10)无符号整数和有符号整数的相同操作可能有不同的指令来完成。

11)访问数组元素,数组下标不要越界,数组索引要乘以元素的大小。

12)不能在ecx=0条件下进行循环,因为会进行232次循环。

2.寄存器和基本指令

寄存器包括通用寄存器、浮点寄存器、向量寄存器、段寄存器等。不同位模式下,有着不同的寄存器集合。

1)16位模式的寄存器:

在这里插入图片描述
在这里插入图片描述

AX 累加寄存器:所有外部设备的输入输出指令只能使用AL或AX做为数据寄存器。

BX 基址寄存器:可以用作数据寄存器;访问存储器时,可以存放被读写的存储单元的地址。是具有双重功能的寄存器。

CX 计数寄存器:可以用作数据寄存器,在循环操作、移位操作时用作寄存器。

DX 数据寄存器:在乘除法中作为数据累加器,在输入输出操作中存放端口的地址。

SI 源变址寄存器:主要用于存放地址,在字符串操作中存放源操作数的偏移地址。变址寄存器内存放 的地址在数据传送完成后,具有自动修改的功能。

DI 目的变址寄存器:主要用于存放地址,在字符串操作中存放目的操作数的偏移地址。

SP 堆栈指针寄存器:存放栈顶的偏移地址,供堆栈操作使用。

BP 基址指针寄存器:存放堆栈内数据的基地址。

Flags 标志寄存器:存放例如OF等数据标志的寄存器。

IP 指令指针寄存器:存放下一个执行指令的寄存器地址。

!

CS 代码段寄存器:执行每一条指令码时,CPU 是非常机械、非常单纯地从 CS:IP 这 2 个寄存器计算得到转换后的物理地址,从这个物理地址所指向的内存地址处,读取一定长度的指令,然后交给ALU去执行。物理地址的计算方式是:CS * 16 + IP。当 CPU 读取一条指令后,根据指令操作码它能够自动知道这条指令一共需要读取多少个字节。指令被读取之后,IP 寄存器中的内容就会自增,指向内存中下一条指令的地址。代码段和数据段,就是内存中的两个地址空间,其中分别存储了指令和数据。

DS 数据段寄存器:数据集中放在位于内存中的某个地址空间中,这块地址空间是一个段,称作数据段。数据段的段寄存器是 DS,当 CPU 执行一条指令,这条指令需要访问数据段时,就会把 DS 这个数据段寄存器中的值左移 1 位之后得到的地址,当做数据段的基地址。

ES 附加数据段寄存器

SS 堆栈段寄存器

在这里插入图片描述

浮点栈寄存器:ST(0)-ST(7)

2)32位模式寄存器

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3)64位模式寄存器

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.寻址模式:

1)16位模式下的寻址:

A.基于代码段的寻址:代码段和数据段都是通过[基地址 + 偏移地址]的方式进行寻址;基地址都放在各自的段寄存器中,CPU 会自动把段寄存器的值,左移 1 位之后,作为段的基地址;偏移地址决定了段中的每一个具体的地址,最大偏移地址是 16 个 bit 1,也即是 64KB 的空间。

立即数寻址
mov ax,1064H(操作数1064H包含在指令中)

寄存器寻址
mov ax,bx,(操作数放在寄存器bx中)

B.存储器寻址:操作数位于内存中,借助立即数,基址寄存器,变址寄存器等进行寻址。

直接寻址
mov ax,[2000H],(操作数放在内存中,指令中直接给出内存地址2000H,这个地址是偏移地址,偏移地址+段地址=物理地址)

寄存器间接寻址
mov bx,[di] (操作数的地址放在寄存器中)。
若有效地址用SI、DI和BX等之一来指定,则其缺省的段寄存器为DS;若有效地址用BP来指定,则其缺省的段寄存器为SS(即:堆栈段)。该寻址方式物理地址的计算方法:物理地址=16×DS + SI/BX/DI 物理地址=16×SS+ BP

寄存器相对寻址
mov ax,[si+100h] (操作数在内存中,其有效地址是一个基址寄存器(BX、BP)或变址寄存器(SI、DI)的内容和指令中的8位/16位偏移量之和)(传送的是地址,最后分配的还是内容)。

基址变址寻址方式
MOV AX,[BX+SI] (取基址和变址的内容之和的地址的内容传送给它,不要忘记段地址)

相对基址变址寻址方式
MOV AX, [BX+SI+200H]

2)32位模式下的寻址:

立即数寻址
mov eax,1064H(操作数1064H包含在指令中)

寄存器寻址
mov eax,ebx,(操作数放在寄存器bx中)

内存寻址
在这里插入图片描述

地址中寄存器的书写顺序决定该寄存器是基址寄存器,还是变址寄存器如:[EBX+EBP]中的EBX是基址寄存器,EBP是变址寄存器,而[EBP+EBX]中的EBP是基址寄存器,EBX是变址寄存器;默认段寄存器的选用取决于基址寄存器。

在这里插入图片描述

基址寄存器是EBP或ESP时,默认的段寄存器是SS,否则,默认的段寄存器是DS;在指令中,如果使用段前缀的方式,那么,显式段寄存器优先。

3)64位模式下的寻址:

RIP相对寻址:相对于32位IP进行寻址。
64位模式下的32位绝对寻址(直接寻址):将小于231的32位地址扩展到64位。
64位绝对寻址(直接寻址)
相对于64位基寄存器的寻址:基寄存器+变址寄存器*比例因子+偏移常量

4.指令码格式

1)前缀:改变操作码的含义(0-5字节)

2)操作码:操作的含义(1-3字节)

3)mod-reg-r/m字节:mod指定寻址模式2bit,reg指定第一个操作数3bit,r/m指定第二个操作数
3bit(0-1字节)

4)SIB字节:指令中有mod-reg-r/m字节时,进行复杂内存操作数地址索引

5)偏移:值与基址寄存器或者变址寄存器相加

6)立即操作数:数据常数

5.ABI

应用程序二进制接口:将汇编与高级语言结合起来时,要适当遵守的ABI标准。

1)寄存器使用

在这里插入图片描述

2)数据存储

函数中声明的变量和对象存储在堆栈上,相对于堆栈指针进行寻址。

全局和静态数据存储在数据段中,32位系统中32位绝对地址,64位系统中使用32位RIP相对寻址。

malloc在堆上分配空间,new在自由存储区分配空间。

3)函数调用约定

16位模式下

堆栈传递函数参数,第一个参数在低地址,堆栈由调用者清理,8/16位大小参数用一个字的堆栈空间,大于16位的参数以小端形式存储,函数返回值由寄存器传递,AL(8bit),AX(16bit),DX:AX(32bit),AX(bool),ST(0)(浮点)。

32位模式下

根据于以下调用约定使用堆栈传递参数
在这里插入图片描述

32位大小或更小的参数使用4个字节的堆栈空间。大于32位的参数以小端点形式存储,并由4对齐。函数返回值由寄存器传递,AL(8bit),AX(16bit),EAX(32bit 整数、引用、bool、指针),EDX:EAX(64bit),ST(0)(浮点)

64位windows模式下

第一个参数是整数,则在RCX中传输;是浮点数或双精度浮点数,则在XMM0中传输。

第二个参数在RDX或XMM1中传输。

第三个参数在R8或XMM2中传输。

第四个参数在R9或XMM3中传输。

请注意,如果使用XMM0,则RCX不用于参数传输,反之亦然。无论类型如何,在寄存器中传输不
能超过四个参数。其他参数都在堆栈上传输,第一个参数在最低地址,并以8对齐。成员函数
以“this”作为第一个参数。返回值在RAX或XMM0中。

调用者还必须在堆栈上分配32个字节的可用空间,即使是没有参数的函数。如果需要的话,被调用的函数可以在这里保存四个参数寄存器。调用者必须清理阴影空间。

64位Linux模式下

前6个整数参数分别在RDI、RSI、RDX、RCX、R8、R9中传输。

前8个浮点参数在XMM0 - XMM7中传输。

寄存器中最多可以同时传输14个参数。其他参数在堆栈上传输,第一个参数在最低地址,并对齐为8。返回值在RAX或XMM0中。没有阴影空间。从[RSP-1]到[RSP-128]的地址范围称为红色区域。

6.将汇编语言与高级语言结合的方法

1)intrinsic function

intrinsic函数以类似内联函数(inline function)的方式封装了汇编指令,使得用户只需了解函数的功能,而不用关注具体的实现。用于编写系统代码、编写C++中没有的简单指令、编写向量操作。如用于输入输出的intrinsic函数 __inbyte, __inword, __indword, __outbyte, __outword, __outdword。

2)inline assembl

内联汇编是在高级语言内部嵌入汇编代码的成分。在实现某些功能的时候,高级语言针对某些情形的境况还是不太够,高级语言可以利用汇编语言的硬件亲和性来亲近底层开发。

3)assembler

其原理是编写一个或多个包含程序中最关键的功能的程序集文件,并在C++中编写不那么关键的部分。然后,将不同的模块链接到一个可执行文件中。
静态链接库:将多个汇编文件收集到函数库中。windows平台下,通过使用库管理器(例如,lib.exe)将一个或多个*.obj文件合并成一个*.lib文件来构建一个静态链接函数库。
动态链接库:静态链接和动态链接的区别在于,静态链接库被链接到可执行程序文件中,因此可执行文件只包含库的必要部分的副本。一个动态链接库(*.dll)作为一个单独的文件分发,它在运行时由Windows系统中的可执行文件加载。

优化

1.优化速度

1)找到代码中最重要的部分,对最耗时的部分进行优化。

有时最耗时的部分不是代码的运行部分,而是其他部分的处理。分析器(profiler)能够指出代码中最重要的部分。如果没有分析器,可设置计数变量,记录各部分执行次数,次数越多执行时间越多。或者可以在重要的部分使用更优化的算法提升速度。

2)乱序执行

前面代码的执行结果对后面代码的执行没有影响,处理器支持这部分代码乱序执行。

mov eax, [mem1] 
imul eax, 6 
mov [mem2], eax 
mov ebx, [mem3] 
add ebx, 2 
mov [mem4], ebx 

3)寄存器重命名

为同名的逻辑寄存器分配不同的物理寄存器,可以提升使用相同寄存器代码的执行速度。

mov eax, [mem1] 
imul eax, 6 
mov [mem2], eax 
mov eax, [mem3] 
add eax, 2 
mov [mem4], eax 

4)指令分割

指令由子功能的指令代替,因此原指令后的操作可在子指令执行结束时执行。

push eax                                                      
call SomeFunction 

替换为

sub esp,4
call SomeFunction 
mov [esp],eax

5)执行单元

CPU含有整数算数单元,浮点算数单元,内存读单元,内存写单元,因此在一个时钟周期内,cpu能进行算数操作、浮点操作、内存操作等不同的操作。

6)流水线

计算机组成原理介绍的流水线能够提高吞吐量。

7)指令获取

减少指令长度,减少jump指令数目,指令对齐为16字节块或32字节块等能缩短指令获取时间。

8)指令解码

限制指令的前缀数目,前缀数目过多则降低解码速度

9)打破依赖链

打破指令对前面指令执行结果的依赖,有助于2)中乱序执行。
i)

double list[100], sum = 0.; 
	for (int i = 0; i < 100; i++) sum += list[i]; 
double list[100], sum1 = 0., sum2 = 0., sum3 = 0., sum4 = 0.; 
for (int i = 0; i < 100; i += 4) { 
sum1 += list[i];
sum2 += list[i+1]; 
sum3 += list[i+2]; 
sum4 += list[i+3]; 
} 
sum1 = (sum1 + sum2) + (sum3 + sum4); 

ii)

     y = (a + b) + (c + d) 

10)常用的分支放在前面,避免进行分支带来的时间开销

Func1: 
cmp eax,567 
je L1 
; frequent branch 
ret 
L1: ; rare branch 
ret

11)如果函数的调用后紧接返回指令,则将这两条指令替换为一条跳转到函数的指令

Func1: 
... 
call Func2
ret 


Func1: 
... 
jmp Func2 

12)通过复制代码消除无条件跳转

Loop1: 
	cmp [edx+eax*4], eax  
	je ElseBranch 
	... 
	jmp End_If 
ElseBranch: 
...  
End_If: 
	add eax, 1  
	jnz Loop1 
	
	mov esp, ebp ; Function epilog 
	pop ebp 
	ret 

Loop1:  
	cmp [edx+eax*4], eax 
	je ElseBranch 
	... ; First branch 
	add eax, 1  
	jnz Loop1 
	jmp AfterLoop 
ElseBranch: 
	...  
	add eax, 1  
	jnz Loop1 
AfterLoop: 
	mov esp, ebp ; Function epilog 
	pop ebp 
	ret 

13)用条件移动替换条件跳转,消除条件跳转分支预测错误带来的时间开销

a = b > c ? d : e; 
mov eax, [b] 
cmp eax, [c] 
jng L1 
mov eax, [d] 
jmp L2 
L1: 
      mov eax, [e] 
L2: 
      mov [a], eax 

mov eax, [b] 
cmp eax, [c] 
mov eax, [d] 
cmovng eax, [e] 
mov [a], eax 

14)用位操作替换条件跳转指令

 if (b > a) b = a; 
sub eax, ebx ; = a-b 
sbb edx, edx ; = (b > a) ? 0xFFFFFFFF : 0 
and edx, eax ; = (b > a) ? a-b : 0 
add ebx, edx ; Result is in ebx

2.优化内存访问

1)高速缓存

访问第一层cache花费3个时钟周期,访问第二层cache花费时钟周期数量级为10。

2)主存

访问主存的数据花费时钟周期数量级为100。

3)内存和高速缓存

内存告诉缓存缺失目标数据则消耗更多时钟周期,如内存数据已经交换到磁盘上、高速缓存缺失目标数据等。

4)跟踪缓存

存储微操作的代码。

5)代码缓存

存储原始指令代码。当代码重要部分存储在不超过代码缓存的连续内存空间中性能最好。

6)微操作缓存

可以存储微操作

7)对齐数据

对齐数据指令为 ALIGN n
在这里插入图片描述

在这里插入图片描述

8)对齐代码

对齐代码为16等

9)组织数据提升缓存

当数据存储在内存中连续区域时性能最好。数据可存储在堆栈空间中,浮点立即数存储在代码中。

10)组织代码提升缓存

如果代码的关键部分包含在一个不大于代码缓存的连续内存区域中,则代码缓存的工作效果最好。

11)高速缓存控制指令

当高速缓存缺失时,可用 MOVNTI, MOVNTQ, MOVNTDQ, MOVNTPD, MOVNTPS指令阻止从内存中读取、修改、写回整个缓存行。

3.优化循环

1)最小化循环的经常开销

循环的经常开销指跳转至循环开始和跳出循环的指令 。可以将分支指令放置在末尾消除分支。可以将i++,cmp i,n替换成 sub n,1消除cmp指令。当数组需要i索引时,i不能消除,从-n到0计数,从0到n访问元素。或从-4n到0计数,从0到4n进行访问。

2)归纳变量

浮点值依赖于整数计数变量,则重新设置一个浮点变量。比如x = 0.0 ,x+=0.01比i = 1,i++,i*0.01更快。

3)移动循环不变代码

循环内没有更改任何结果的表达式应该移出循环

4)循环内的指令获取、解码和退出

指令获取:将循环条目对齐16,减小指令大小。
指令解码:遵守cpu相关的解码规则,避免会生成两个以上微操作的复杂指令。
避免循环内的跳转和调用,循环调用的子例程应该内联
避免循环内的分支,会干扰循环跳出的预测。

5)在执行单元中平均分配微操作

6)循环展开

一个做了n次重复的循环可以被一个重复了n / r次的循环所取代,并对每次重复进行r次计算,其中r是展开因子。N最好可以被r整除。

问题指令

1)LEA

lea rax, [rbx+8*rcx-1000] 

比下面指令快

; Example 16.1b 
mov rax, rcx 
shl rax, 3 
add rax, rbx 
sub rax, 1000 

2)INC和DEC

INC和DEC指令不修改进位标志,但它们确实修改了其他算术标志。在一些cpu上,只写入部分标志寄存器需要花费额外的µop。在优化速度时,应该使用ADD和SUB。在优化大小或预期没有惩罚开销时,使用INC和DEC。

3)XCHG

耗时

4)Bit test

BT、BTC、BTR和BTS指令最好被TEST、and、OR、XOR指令取代

5)整数乘法

带有常数的乘法可以被LEA指令替代,如IMUL EAX,5在64位模式下能被LEA EAX,[RAX+4RAX]替代,在32位模式下能被 LEA EAX,[EAX+4EAX]替代。

6)除法

除以2的次幂可以通过移位实现。
除以浮点数是用乘以浮点数的倒数替换,除以整数可以将倒数乘以2n,将结果向右移动n位。

性能测试

1)许多编译器都有一个分析器

它可以测量程序中每个函数被调用的次数以及它需要多长时间。这对于找到程序中的任何热点都非常有用。如果某个特定的热点占总执行时间的比例很大,那么这个热点应该是您的优化工作的目标。许多分析器不是很精确不足以微调一小部分代码。
测试一段代码速度的一种更准确的方法是使用所谓的时间戳计数器。这是一个内部的64位时钟计数器,它可以使用指令RDTSC(读取时间戳计数器)读入EDX: EAX。时间戳计数器在CPU时钟频率上计数,这样一个计数等于一个时钟周期,这是最小的相关时间单位。英特尔处理器有一个“核心时钟计数器”,它以实际的核心时钟频率来计数。核心时钟计数器给出了更一致和可重复性的结果。

2)如果您想找出一个函数的哪个版本的性能最好

那么仅在一个多次调用该函数的小测试程序中测量时钟周期是不够的。这样的测试不太可能给出缓存丢失的真实度量,因为测试程序可能使用比缓存大小更少的内存。例如,一个隔离测试可能显示展开循环是有利的,而在最终程序中插入函数的测试显示在展开循环时大量缓存丢失。因此,在评估函数的性能时,不仅要计算时钟周期,而且要考虑它在代码缓存、数据缓存和分支目标缓冲区中使用的空间。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SZheniu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值