怎样优化Pentium系列处理器的代码.docx

上传人:b****8 文档编号:30643770 上传时间:2023-08-18 格式:DOCX 页数:23 大小:33.64KB
下载 相关 举报
怎样优化Pentium系列处理器的代码.docx_第1页
第1页 / 共23页
怎样优化Pentium系列处理器的代码.docx_第2页
第2页 / 共23页
怎样优化Pentium系列处理器的代码.docx_第3页
第3页 / 共23页
怎样优化Pentium系列处理器的代码.docx_第4页
第4页 / 共23页
怎样优化Pentium系列处理器的代码.docx_第5页
第5页 / 共23页
点击查看更多>>
下载资源
资源描述

怎样优化Pentium系列处理器的代码.docx

《怎样优化Pentium系列处理器的代码.docx》由会员分享,可在线阅读,更多相关《怎样优化Pentium系列处理器的代码.docx(23页珍藏版)》请在冰豆网上搜索。

怎样优化Pentium系列处理器的代码.docx

怎样优化Pentium系列处理器的代码

怎样优化Pentium系列处理器的代码

Copyright©1996,2000byAgnerFog.Lastmodified2000-07-03.

云风(CloudWu)译翻译中...(13.3%)

 

目录

1.简介

2.文献

3.高级语言中调用汇编函数

4.调试及校验

5.内存模式

6.对齐

7.Cache

8.第一次vs重复运行

9.地址生成互锁(AGI)(PPlain及PMMX)

10.配对整数指令(PPlain及PMMX)

1.完美的配对

2.有缺陷配对

11.将复杂指令集分割为简单指令(PPlain及PMMX)

12.前缀(PPlain及PMMX)以下待译...

13.PPro,PII及PIII流水线综述

14.指令解码(PPro,PII及PIII)

15.取指令(PPro,PII及PIII)

16.寄存器重命名(PPro,PII及PIII)

1.消除依赖性

2.寄存器读延迟

17.乱序执行(PPro,PII及PIII)

18.引退(PPro,PII及PIII)

19.部分(Partial)延迟(PPro,PII及PIII)

1.部分寄存器延迟

2.部分标记延迟

3.移位和旋转后的标记延迟

4.部分内存延迟

20.依赖环(PPro,PII及PIII)

21.寻找瓶颈(PPro,PII及PIII)

22.分支和跳转(所有的处理器)

1.PPlain的分支预测

2.PMMX,PPro,PII及PIII的分支预测

3.避免跳转(所有的处理器)

4.避免使用标记的条件跳转(所有的处理器)

5.将条件跳转替换成条件赋值(PPro,PII及PIII)

23.减少代码长度(所有的处理器)

24.规划浮点代码(PPlain及PMMX)

25.循环优化(所有的处理器)

1.PPlain及PMMX中的循环

2.PPro,PII及PIII中的循环

26.有问题的指令

1.XCHG(所有的处理器)

2.带进位标记的循环移位(所有的处理器)

3.字符串指令(所有的处理器)

4.位测试(所有的处理器)

5.整数乘法(所有的处理器)

6.WAIT指令(所有的处理器)

7.FCOM+FSTSWAX(所有的处理器)

8.FPREM(所有的处理器)

9.FRNDINT(所有的处理器)

10.FSCALE及幂函数(所有的处理器)

11.FPTAN(所有的处理器)

12.FSQRT(PIII)

13.MOV[MEM],ACCUM(PPlain及PMMX)

14.TEST指令(PPlain及PMMX)

15.Bitscan(PPlain及PMMX)

16.FLDCW(PPro,PII及PIII)

27.特别主题

1.LEA指令(所有的处理器)

2.除法(所有的处理器)

3.释放浮点寄存器(所有的处理器)

4.浮点指令与MMX指令的转换(PMMX,PII及PIII)

5.浮点转换为整数(所有的处理器)

6.使用整数指令做浮点运算(所有的处理器)

7.使用浮点指令做整数运算(PPlain及PMMX)

8.数据块的移动(所有的处理器)

9.自修改代码(所有的处理器)

10.检测处理器类型(所有的处理器)

28.指令速度列表(PPlain及PMMX)

1.整数指令集

2.浮点指令集

3.MMX指令集(PMMX)

29.指令速度及微操作失败列表(PPro,PII及PIII)

1.整数指令集

2.浮点指令集

3.MMX指令集(PII及PIII)

4.XMM指令集(PIII)

30.速度测试

31.不同的微处理器间的比较

 

1.简介

这本手册细致的描述了怎样写出高度优化的汇编代码,着重于讲解Pentium®系列的微处理器.

这儿所有的信息都基于我的研究.很多人为这本手册提供了有用的信息和错误矫正,而我在获得任何新的重要信息后都更新它.因此这本手册比其它类似的信息来源都更准确,详尽,精确和便于理解,而且它还包含了许多其它地方找不到的细节描述.这些信息使你能够用多种方法精确统计一小段代码花掉的时钟周期数.但是,我不能保证手册里所有的信息都是精确的:

一些时间测试等是很难或者不可能精确测量的,我看不到Intel手册作者拥有的内部技术文档资料.

这本手册讨论了Pentium处理器的下列版本:

缩写

名字

PPlain

plain老式Pentium(没有MMX)

PMMX

有MMX的Pentium

PPro

PentiumPro

PII

PentiumII(包括Celeron和Xeon)

PIII

PentiumIII(包括一些相当的CPU)

这本手册中使用了MASM5.10的汇编语法.没有什么官方的X86汇编语言,但这是最接近你能接受的标准,因为几乎所有的汇编器都有MASM5.10兼容模式.(然而我不推荐使用MASM的5.10版本.因为它在32位模式下有严重的Bug.最好是使用TASM或者MASM的后续版本).

手册里的一些评语好象是对Intel的批评.但这并不是说其它的产品会好一些.Pentium系列的微处理器能在评测分中取得更好一点的比较值,有更好的文档,和更多的可测试特性.由于这些原因,不会有我或者其他人做同类商品的比较测试.

汇编语言编程比用高级语言要复杂的多.制造Bug是很容易的,但是找到Bug却很难.现在已经提醒你了!

我假定读者已经有汇编编程的经验.没有的话,请在做复杂的优化前读一些汇编的书并且写些代码获得些汇编的经验.

PPlain和PMMX芯片的硬件设计有许多专门为一些常用指令或者指令对设计的特性,而不是使用那些一般的优化模块.因此,为这个设计优化软件的规则很复杂,且有很多的例外,但是这样做可能获得实质性的好处.PPro,PII和PIII处理器有非常不同的设计,它们会利用乱序执行来做许多的优化工作,但是处理器的这些个设计带来了许多潜在的瓶颈,因此为这些处理器进行手工优化将得到许多的好处.Pentium4处理器也用了另外一种设计,奔腾4的优化指导路线和前面的版本非常的不同了.这个手册没有禳括奔腾4-读者请自己查阅Intel的手册.

在把你的代码转为汇编的之前,确认你的算法是足够优化的.通常你可以通过优化算法来将代码效率提高的比转成汇编获得的效率多的多.

第二,你必须找到你的程序里最关键的部分.通常99%的CPU时间花在程序最里面的循环中.在这种情况下,你只要优化这个循环并把其它的所有东西都用高级语言写.一些汇编程序员将大量的精力花在了他们程序的错误的部分上,他们努力得到的唯一结果就是程序变的更加难以调试和维护了.

如果你的程序的关键部分并不那么明显,你可以用profil来找.如果发现瓶颈在磁盘操作,然后你就可以试着修改程序使磁盘操作集中连续,提高磁盘缓冲的命中率,而不是用汇编来写代码.如果瓶颈在图象输出,那么你就可以尝试找到一种方法来减少调用图象函数的次数.一些高级语言编译器对于指定的处理器提供了相对好的优化,但是手工优化将做的更好.

请不要将你的编程问题寄给我.我不会帮你做家庭作业的!

祝你在后面的阅读中好运!

2.文献

在Intel的www站上,打印的文本或者CD-ROM上都有很多有用的文献和教程.建议你研究一下这些文档来对微处理器的结构有些认识.然而,Intel的文档也不总是对的-尤其是那些教程有很多错误(显然,Intel的那些人没有测试他们的例子).

这里我不给出URL,因为文件的位置经常的改变.你可以利用或者www.agner.org/assem上的链接上的搜索工具找到你要的文档.

一些文档是.PDF格式的.如果你没有显示或者打印PDF的工具,可以去下载Acrobat文件阅读器.

使用MMX和XMM(SIMD)指令优化专门的程序在几本使用手册里都有描述.各种手册和教程都有描述其指令集.

VTUNE是Intel用来优化代码的软件工具我没有测试它,因此这里不于评价.

还有很多比Intel的更多的更有用的信息.在新闻组comp.land.asm.x86的FAQ里有把这些资源列出来.其它的internet上的资源在www.agner.org/assem上也有链接.

3.在高级语言里调用汇编函数

你可以使用在线汇编或者用汇编写整个子程序然后再连接到你的工程中.如果你选择后者,建议你用可以将高级语言直接编译成汇编的编译器.这样你可以得到函数的正确的调用原型.所有的C++编译器都能做这个工作.

传递参数的方法取决于调用形式:

 调用方式 

 参数在堆栈里的次序 

 参数由谁来移去 

 _cdecl 

 第一个参数在低位地址 

 调用者 

 _stdcall 

 第一个参数在低位地址 

 子程序 

 _fastcall 

 编译器指定 

 子程序 

 _pascal 

 第一个参数在高位地址 

 子程序 

函数调用原型和被编译器命名的函数名可能非常的复杂.有很多不同的调用转换规则,不同的编译器也互不兼容.如果你从C++里调用汇编语言的子程序,最好的方法是将你的函数用extern"C"和_cdel定义来兼容性和一致性.汇编代码的函数名前面必须带一个下划线(_)并且在外面编译时加上大小写敏感的选项(选项-mx).例如:

;extern"C"int_cdeclsquare(intx);

_squarePROCNEAR;整型平方函数

PUBLIC_square

MOVEAX,[ESP+4]

IMULEAX

RET

_squareENDP

如果你需要重载函数,重载操作符,方法,和其它C++专有的东西,就必须在C++里先写好代码再用编译器编译成汇编代码以获得正确的连接信息和调用模型.这些随着编译器的不同而不同而且很少列出文档.如果你希望汇编函数用其它的调用原型而不是extern"C"及_cdecl的,又可以被不同的编译器调用,那么你需要为每个编译器写一个名字.例如重载一个square函数:

;intsquare(intx);

SQUARE_IPROCNEAR;整数square函数

@square$qiLABELNEAR;Borland编译器的连接名字

?

square@@YAHH@ZLABELNEAR;Microsoft编译器的连接名字

_square__FiLABELNEAR;Gnu编译器的连接名字

PUBLIC@square$qi,?

square@@YAHH@Z,_square__Fi

MOVEAX,[ESP+4]

IMULEAX

RET

SQUARE_IENDP

;doublesquare(doublex);

SQUARE_DPROCNEAR;双精度浮点square函数

@square$qdLABELNEAR;Borland编译器的连接名字

?

square@@YANN@ZLABELNEAR;Microsoft编译器的连接名字

_square__FdLABELNEAR;Gnu编译器的连接名字

PUBLIC@square$qd,?

square@@YANN@Z,_square__Fd

FLDQWORDPTR[ESP+4]

FMULST(0),ST(0)

RET

SQUARE_DENDP

这个方法能够工作是因为所有这些编译器对重载的函数都缺省使用_cdecl调用.然而,对于方法(成员函数),对于各种编译器,甚至调用方式都不一样(Borland和Gnu编译器使用_cdecl方式,'this'指针是第一个参数,而Microsoft使用_stdcall方式,而'this'指针放在ecx里).

通常来说,当你使用了下列东西时,不要指望不同的编译器在目标文件级别可以兼容:

longdouble,成员指针,虚机制,new,delete,异常,系统函数调用,或者标准库函数.

16位模式DOS和Windows,CorC++的寄存器使用:

AX是16位返回值,DX:

AX是32位返回值,ST(0)是浮点返回值.寄存器AX,BX,CX,DX,ES数学标记可以被过程改变;其它的寄存器必须保存和恢复.一个过程要不改变SI,DI,BP,DS和SS的前提下才不会影响另一个过程..

32位模式Windows,C++和其它编程语言下的寄存器使用:

整数返回值放在EAX,浮点返回值放在ST(0).寄存器EAX,ECX,EDX(没有EBX)可以被过程修改;其它的寄存器必须保留和恢复.段寄存器不能被改变,甚至不能被临时改变.CS,DS,ES,和SS都指向当前的段组.FS被操作系统使用,GS没有使用,但是被保留.标记位可以在下面的限制下被过程改变:

方向标志缺省是0.方向标志可以暂时的修改,但是必须在任何的调用或者返回前清除.中断标志必须清除.浮点寄存器堆栈在过程入口处是空的,返回时也应该是空的,除了ST(0)如果被用于返回值则除外.MMX寄存器可以被改变但是在返回前或者在调用可能使用浮点运算的过程前必须用EMMS清一下.所有的XMM寄存器都可以被过程修改.在XMM寄存器里的传递参数和返回值的描述在Intel的应用文档AP589.一个过程可以在不改变EBX,ESI,EDI,EBP和所有的段寄存器的前提下被另一个过程调用.

4.调试和校验

正如你已经发现的,调试汇编代码非常的困难和容易受到挫折.我建议你先把你需要优化的小段代码用高级语言写成一个子程序.然后写个小的测试程序可以充分测试你的这个子程序.确认测试程序可以测试到所有的分支.

当高级语言的子程序可以工作了,你再把它翻译成汇编代码.

现在你可以开始优化了.每次你做了点修改都应该运行测试程序看看能不能正确工作.将你所有的版本都标上号并保存起来,这样在发现测试程序检查不到的错误(比如写到错误的地址)时可以回头来重新测试.

用第30章里提到的所有方法或者用测试程序测试最你的程序中最关键的部分.如果代码比你期望的速度慢的太多,最可能的原因是:

cache失效(第7章),未对齐操作(第6章),第一次运行消耗(第8章),分支预测失败(第22章)取指令问题,(第15章),寄存器读延迟(第16章),或者是过长的依赖环(第20章).

高度优化的代码将变得对其他人非常难读懂,甚至对你日后在读也有困难.为了使维护代码变为可能,将代码组织为一个个小的良好预定义宏和清楚注释的逻辑段(过程或者宏)就非常重要.代码越复杂艰涩,写下好的文档就越重要.

5.内存模式

Pentium主要为32位代码设计,16位代码的性能很差.将你的代码和数据分段也会明显的降低性能,因此通常你应当使用32位平坦模式,并且使用支持这种模式的操作系统.如果不特别注明,这本手册里所有的例子都使用32位平坦内存模式.

6.对齐

内存里的所有数据都必须按照下表将地址对齐到可以被2,4,8或16整除的位置:

 

对齐

 操作数据长度 

 PPlain及PMMX 

 PPro,PII及PIII 

 1(byte) 

1

1

 2(word) 

2

2

 4(dword) 

4

4

 6(fword) 

4

8

 8(qword) 

8

8

 10(tbyte) 

8

16

 16(oword) 

n.a.

16

在PPlain和PMMX上,访问未对齐数据在4字节边界线交错的时候将至少有3个时钟周期的额外消耗.当cache边界线被交错的时候损耗更大.

在PPro,PII和PIII上,当cache边界线交错时,未对齐数据将消耗掉6-12个时钟周期.小于16字节的未对齐操作数,没有在32字节边界上交错时将没有额外的损耗.

在dword栈上以8或16对齐数据可能会有问题.常见的情况是设置对齐的帧指针.对齐本地数据的函数可以是这样:

_FuncWithAlignPROCNEAR

PUSHEBP;前续代码

MOVEBP,ESP

ANDEBP,-8;以8来对齐帧指针

FLDDWORDPTR[ESP+8];函数参数

SUBESP,LocalSpace+4;分配本地空间

FSTPQWORDPTR[EBP-LocalSpace];在对齐了的空间保存一些东西

...

ADDESP,LocalSpace+4;结束代码.恢复ESP

POPEBP;(PPlain/PMMX上有AGI延迟)

RET

_FuncWithAlignENDP

虽然对齐数据永远是重要的,但是在PPlain和PMMX上对齐代码却没有必要.PPro,PII及PIII上对齐代码的原则在第15章阐述.

7.Cache

PPlain和PPro有代码用的8kb片内cache(一级cache),8kb数据cache.PMMX,PII和PIII则有16kb代码cache和16kb数据cache.一级cache里的数据可以在一个时钟周期内读写,cache未命中时将损失很多时钟周期.理解cache是怎样工作的非常重要,这样才能更有效的使用它.

数据cache由每行32字节的256或512行组成.每次你读数据未命中,处理器将从内存读出一整条cache行.cache线总是在物理地址的32字节对齐.当你从一个可以被32整除的地址读出一个字节,下31字节的读写就不会有多余的消耗.你可以对齐数据在32字节块里来从中获得好处.例如,如果你有一个循环要操作两个数组,你就可以将两个数组合并成一个结构数组,这样被使用的数据就储存在一起了.

如果数组或者其它数据结构是32字节的倍数,你最好将其按32字节对齐.

cache是联想设置的.这就是说cache行不能随心所欲的设置到指定内存地址.每个cache行有7-bit设定值来匹配物理地址的5到11位(0-4位就是cache行的32字节).PPlain和PPro可以有两条cache行对应128个设定值中的一个值,因此任两条cache行可以设定到任何的RAM地址.PMMX,PII和PIII则可以有4个.

其结果是cache不能保留超过2个或4个的地址的5-11位相同的不同数据块.你可以用用以下方法检测两个地址是否有相同的设定值:

截掉地址的低5位得到可以被32整除的数值.如果两个截断地址之差是4096(=1000H)的倍数,这两个地址就有相同的设定值.

让我用下面的一小段代码来说明一下,这里ESI放置了一个可以被32整除的地址:

AGAIN:

MOVEAX,[ESI]

MOVEBX,[ESI+13*4096+4]

MOVECX,[ESI+20*4096+28]

DECEDX

JNZAGAIN

这3个地址都有相同的设定值,因为不同的截断地址都是4096的倍数.这个循环在PPlain和PPro上将运行的相当慢.当你读ECX的时候,没有适当设定值的空闲cache行,因此处理器将使用最近最少使用的两组cache行中的一个,那就是EAX使用的那个,然后从[ESI+20*4096]到[ESI+20*4096+31]读出数据来填充并读入ECX.下一步,当读EAX时,你将发现为EAX保存数据的cache行已经被丢弃了,所以将使用最近最少使用的cache行,那就是保存EBX数值的那个了,等等..这将会产生大量的cache丢失,这个循环大概开销60个时钟周期.如果第3行改成:

MOVECX,[ESI+20*4096+32]

这样我们就会在32字节边界上交错,因此我们没有和前两行相同的设定值,这样为这三个地址分别指定cache行就没有什么问题了.这个循环仅仅消耗3个时钟周期(除了第一次运行)-一个相当大的提高!

如刚才提到的,PMMX,PII和PIII有4向cache行,因此你可以有4个相同设定值的cache行.(一些Intel文档错误的说PII的cache是两向的).

检测你的数据地址是否有相同的设定值可能非常困难,尤其是它们分散在不同的段里.你能避免这种问题的最好能做的是将关键部分使用的所有数据都放在一个不超过cache大小的连续数据块里,或者放在两个不超过cache一半大小连续数据块(例如一个静态数据块,一个堆栈数据块)这样你的cache行就一定会高效使用.

如果你的代码的关键部分要操作很大的数据结构或者随机数据地址,你可能会想保存所有常用的变量(计数器,指针,控制变量等)在一个单独的最大为4k的连续块里面,这样你就有一个完整的cache行集来访问随机数据.既然你通常需要栈空间来为子程序保存参数和返回地址,最好的做法是复制所有的常用静态数据到堆栈,如果它们被改变,就在关键循环外复制回去.

读一个不在一级缓存里的数据将导致从二级缓存读入整个cache行,这大约要消耗200ns(在100MHz系统上是20时钟周期,或是200MHz上的40个周期),但是你最先需要的数据将在50-100ns后准备好.如果数据也不在二级缓存,你将会碰到200-300ns的延迟.如果数据在DRAM页边界交错,延迟时间会更长一些.(4/8Mb的72线内存DRAM页大小是1Kb,

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高等教育 > 哲学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1