前言
很多人脑子里都有这么一些概念:
“汇编啊?那是‘高手’们的专利,我用不上”、“我又不和系统打交道,学汇编干嘛啊”、“用汇编写程序的人是白痴(CSDN论坛里看到的原话),太没效率了”。
同意的朋友一定不少!诚然现在的软件已经越来越庞大越,来越复杂,程序开发人员已经远离了那个只和二进制0、1代码,汇编助记符打交道的年代!就算是写系统软件也是如此,PC上的操作系统、编译工具绝大多数代码也是用高级语言完成的!如果现在有人说要完全用汇编在PC上开发软件(不是写程序),那确实不值得大家推崇!但任何问题都要辩证的来看,但如果能在应用高级语言开发软件的过程中合理的应用所学的汇编知识绝对是有百利而无一害(因为这里是说Delphi和汇编,移植等问题不考虑在内)。知识是用来解决实际问题的,所以没有必要排斥汇编,应该让其充分融入到我们应用高级语言开发的过程中去!虽然一味的强调技术至上不对,但也不能因此完全忽略代码细节。最好还是能在开发效率和执行效率中找到一个平衡。
如果掌握了汇编,如果使用其相关知识呢?
首当其冲的当然是利用嵌入汇编编写程序。在Delphi中的嵌入汇编程序很多情况可以参照Win32ASM。但由于嵌入汇编的特殊性,这里并不支持EQU、PROC、STRUC、SEGMENT等伪指令,它们实现的大部分功能已由Object Pascal实现。另外在寄存器的修改方面,除了ESI、EDI、EBP等外,EBX也不能被随便修改,因为在Delphi中用EBX保存Self指针。如果一定要用,必须用栈来保存/恢复寄存器中的原始数据。当然嵌入汇编只是解决问题的手段,不是目的,汇编代码会降低程序的可读性、可维护性能,在同等情况下理应优先考虑如何用高级语言实现。
利用嵌入汇编编写程序自然也不是学习汇编的唯一目的,更重要的掌握了一种从深层次理解程序的方法。即使不直接用汇编写程序,将这种用汇编思考问题的思想融入到用高级语言编写程序的过程中去,汇编一样成为我们分析解决问题的利器。另外汇编知识对于程序调试一样也有着不可替代的作用,在很多情况汇编层的调试可以更快的解决问题。由此可见,有些情况下汇编知识的应用不光是为了提高程序执行效率,有时候也是为了提高开发效率。
许多朋友并没有认真学习过汇编,就开始大喊汇编无用,大学课本无用就显得太不负责任了。有些朋友在大学里只求掌握几种时髦的开发工具和最新的开发技术,而忽视汇编这类基础知识的学习也不见得是明智的做法。相信只要冯.诺伊曼的计算机体系不发生改变,现有的汇编知识必然会有其用武之地。
初级优化篇
说到优化,很多人又不屑一顾了,“现在计算机速度都那么快了,再快那么百分之几有什么意义啊”。这么说确实有些道理,现在的编译器编译后的结果已经是充分优化过了,除了图形图像多媒体等特定软件的开发外、多数情况下刻意的优化确实没必要,但是如果开发人员在编写代码的时候已经具有了优化意识,在完成优化的同时,又能保证了甚至提升开发效率,何乐而不为呢?
当然,算法的设计都是优化的核心,绝大多数情况下,程序的执行效率高低主要由开发人员对程序整体把握,算法的设计等来决定!但有时候针对细节的优化也是有一定意义的!
而且这种优化在很多情况下也并不需要直接通过汇编来写代码实现,但这种情况下却也能体现出掌握汇编知识的优越性!
如下面两个函数:
function GetBit(i: Cardinal; n: Cardinal): Boolean;
begin
Result := Boolean((i shr n) and 1);
end;
function GetBit(i: Cardinal; n: Cardinal): Boolean;
begin
Result := Boolean((1 shl n) and i);
end;
对应的汇编代码:
MOV ECX, EDX
SHR EAX, CL
AND EAX, $01
MOV ECX, EDX
MOV EDX, $01
SHL EDX, CL
AND EAX, EDX
它们的作用一样,都是取i某位的值,为1返回True,0返回False!
表面上看可能都会认为两个函数的执行效率一样,实际上还是有区别的,第一段程序是的移位操作是对i进行的,按照Delphi中默认的调用约定register,此时的i的值是存在寄存器EAX中,移位操作可直接完成;而第二段程序则不同,要对立即数1完成移位操作,必须先将其传送到寄存器,由此也就必然多出一条指令!当然也不是所有情况下,指令少就一定比指令多要快,具体执行时还要考虑指令执行的时钟周期和指令的配对等问题(后面再介绍些),独立出来也说明不了问题,只有在具体代码环境中才好作比较。
一般情况下这种效率上的执行差异实在是太微不足道了,但在编程期间时刻保持着优化的意识绝不是件坏事!如果此类代码位于循环的最里层,N个时钟周期经过大量循环的累积,产生的执行效率差异也可能变的很大!
上面只是个很小的例子,由此可以看出在开发中如果能站在汇编的角度思考一些问题,能在保证开发效率的同时用高级语言编写出更有效率的细节代码!但还有很多时候,细节优化还要用使用嵌入汇编代码来完成,而且有些时候由于嵌入汇编代码应用,还能使代码编写变得更有效率。
如需要将一个32位数的字节顺序颠倒,在Delphi中,完全用高级语言实现怎么做?用移位可以,多次调用内建函数Swap也可以,但是如果想到一条BSWAP指令,这一切变得很简单。
function SwapLong(Value: Cardinal): Cardinal;
asm
BSWAP EAX
end;
注:同上,Value的值是存在寄存器EAX中,而32位数的值也通过EAX返回,所以只需要一句即可。
当然多数的嵌入汇编优化没有这么简单,不过通过大学里所学的那一点点汇编知识也很难做到更深入的优化,也只能通过不断的积累,对比编译后的汇编代码获取经验!好在多数情况下,细节优化并不是程序设计的主体。
但如果所开发程序涉及到图形图像多媒体等方面,还是有必要进行更深入的优化的!好在不管是浮点指令的优化还是应用MMX、SSE、3DNow等完成优化,Delphi6都能提供良好的支持。即使是想早期版本的Delphi支持这些CPU扩展指令集或者想要支持以后新的CPU指令集,利用Delphi在嵌入汇编中所支持的DB、DW、DD、DQ等四条汇编指令(在Borland的Delphi6官方语言手册里只说支持DB、DW、DD)插入相关指令的数值表示也能灵活的实现。
如:
DW $A20F //CPUID
DW $770F //EMMS
DB $0F, $6F, $C1 //MOVQ MM0, MM1
了解指令只是基础,在围绕FPU,MMX,SSE设计完算法后,想更深一步的进行优化,还必须了解一些CPU本身的技术特性。
先看看下面两段代码:
asm
ADD [a], ECX
ADD [b], EDX
end
asm
MOV EAX, [a]
MOV EBX, [b]
ADD EAX, ECX
ADD EBX, EDX
MOV [a], EAX
MOV [b], EBX
end
第二个效率高?错了,如上面说的,指令少不意味着执行效率高,查查相关资料可知,第一段代码的两条指令执行的时钟周期为3(每条指令都需要完成读、改、写三步),第二段代码中的6条指令执行的时钟周期都为1。那么说两段代码效率一样?又错了,实际上第二段代码执行效率比第一段代码要高!为什么?因为奔腾级以后的CPU都有两条流水线来执行指令,所以当相邻的两条指令能够完成配对,那么它们就能够同时执行!具体到上面的两段代码来说具体原因又是什么呢?
第一段代码中的两条指令虽然可以完成配对,但需要的总执行时钟周期为5而不是3,而第二段代码的六条指令可以两两之间并行执行,所以也就导致了这个结果。
说到这里,都是些很浅显的例子,本身给不了大家太多的帮助。如果真的想优化特定程序,还是找些FPU,MMX优化的专题文章看看,或者找来技术手册好好专研专研“乱序执行”和“分枝预测”等技术。只希望各位在上大学的朋友们不要只专注于那些“能赚钱”的开发工具和时髦的新技术,能把更多的时间花在打基础上,有了扎实的基础才能快速掌握新知识、才能用更快的时间掌握新的开发工具、才能...(省略一千字)。
不过话又说回来,知识还是要用来解决实际问题的,如果每天就只在技术细节上做文章,也许能成为一个出色的黑客,但绝对开发不出一流的软件。所以还是要以创造价值为根本目的。所以...不说了,再说下去就真不像技术文章了。^_^
附:程序优化除了考虑执行效率以外,当然也要考虑体积的问题(体积小才能更快的载入内存,更快的完成指令译码等工作),比如清空EAX寄存器都是用SUB EAX, EAX或XOR EAX, EAX而不会用MOV EAX, $0,虽然它们的执行时钟周期都是1,但前者的指令长度(2字节)明显比后者(5字节)短。但因为上面说的都是些细节,所以没提到体积的问题。更多的缩小体积的问题还是交给编译器去解决吧,在编写嵌入ASM代码的同时稍微注意一下就可以了。