使用优化开关

虽然编译器的优化开关被抨击产生BUG代码,我认为没有比放弃它更糟的事了。依我经验,高度优化时产生的问题都是由于优化代码速度而不是代码长度引起的。说实在话,在我让编译器为代码长度优化时我从来没有见过优化开关产生的BUG。
优化开关的好处不是它产生好的代码,一个优秀的汇编语言程序员能做到甚至比优化开关做得更好。优化开关能把你从编译器可能产生的效率不高的代码中拯救出来。下面我用Visual C++来做个例子,你能看见不同的编译器产生的类似的结果。
 int foo( int i )
{
    return i * 2;
}

int main()
{
    if ( foo(7) )
        return 1;
    else
        return 0;
}


缺省情况下,非优化的Visual C++ 4.1用CL FOO.C语句会产生表1的指令,
这两个函数共有0x48字节。
Figure 1 Assembler from Nonoptimized Code

 foo proc
401000: PUSH    EBP
401001: MOV     EBP,ESP
401003: PUSH    EBX
401004: PUSH    ESI
401005: PUSH    EDI
401006: MOV     EAX,DWORD PTR [EBP+08]
401009: ADD     EAX,EAX
40100B: JMP     00401010

401010: POP     EDI
401011: POP     ESI
401012: POP     EBX
401013: LEAVE
401014: RET
foo endp

main proc
401015: PUSH    EBP
401016: MOV     EBP,ESP
401018: PUSH    EBX
401019: PUSH    ESI
40101A: PUSH    EDI
40101B: PUSH    07
40101D: CALL    00401000
401022: ADD     ESP,04
401025: TEST    EAX,EAX
401027: JE      0040103C

40102D: MOV     EAX,00000001
401032: JMP     00401043

401037: JMP     00401043

40103C: XOR     EAX,EAX
40103E: JMP     00401043

401043: POP     EDI
401044: POP     ESI
401045: POP     EBX
401046: LEAVE
401047: RET
main endp
现在,我们打开长度的优化开关(CL /O1 FOO.C):

 foo proc
401000: MOV     EAX,DWORD PTR [ESP+04]
401004: ADD     EAX,EAX
401006: RET
foo endp

main proc
401007: PUSH    07
401009: CALL    00401000
40100E: ADD     ESP,04
401011: CMP     EAX,01
401014: SBB     EAX,EAX
401016: INC     EAX
401017: RET
main endp

噢!打开长度优化开关使生成的代码少了0x18字节,是非优化的33%。如果你比较一下这两段代码,你能看见优化开关减少不必要代码的几个途径。
第一,两个函数(FOO和MAIN)都不要堆栈(PUSH EBP,MOVE EBP,ESP和LEAVE指令)。第二,注册的变量寄存器(EBX,ESI,EDI)都没有用上,所以优化版本不用PUSH和POP 保存它们。第三,在非优化版本里,两个函数都经常用JMP来跳到下一条指令上。这样做不仅无用、占5字节,还打断了CPU的流水线(pipeline),这是应该避免的。第四,main函数里的if语句在优化版本里很聪明地翻译成CMP,SBB,用EAX来返回值;然而,你简直找不到比非优化版本更差的办法了。
在这儿,FCC规则要求我告诉你,你不能时刻期待优化器能有如此成功的效果,上面这个小例子无疑是造作的结果。同时,记住我打开的长度开关是长度的开关,而不是速度。这里的要点是优化开关给了你聪明的代码。
当你比较长度优化和速度优化时,你会发现这两者几乎是同一回事。主要的差别在于,优化速度时编译器会打开内联的函数,例如strcpy等,这样生成的代码就不用调用外部函数。这时速度会加快,但内联函数会增大代码。在最坏情况下,它们会加入一个4KB的页,从而可能引起附加页面错。一个页面错的严重性应该说比你从内联函数得到的好处要大,所以你必须小心权衡你的优化的决定。这个可以作为参考: Microsoft的操作系统编写队伍从长度优化而不从速度优化。

看看你的队列!
除了长度优化给你的更好的代码之外,还有一个很好的理由让你使用 Visual C++的长度优化开关。从不同的OBJ和LIB文件里集成片断(section)时,编译器把每个OBJ文件里的代码和数据从一个偏移量开始放置。对于COFF的OBJ文件 (由Visual C++生成),偏移量是WINNT.H中预定义的IMAGE_SCN_ALIGN_XBYTES,可以是1, 2, 4, 8, 16, 32, 或64字节
Visual C++ 4.1的缺省偏移值是16字节,也就是说, OBJ文件里的每个片断都从第16字节开始放置,在上一个OBJ与下一个OBJ中的空间填满INT 3。最糟情况下,联接后的文件里每个段落之间你会得到15字节INT 3。如果Visual C++能让你选择偏移的量会好一些,但现在并没有提供这个功能。现在,在OBJ之间你必然有16字节的偏移。
如果说这些OBJ之间的填充不足以称为"空间杀手",一个看起来无害的Visual C++编译选项如果你使用不恰当会。想一下Visual C++文件是怎么介绍/Gy开关的: "本选项按照COMDAT格式生成函数包以实现函数级联接"。在英语里,这意味着联接器将从一个OBJ里取出所需要的函数,而不是把整个OBJ联接进来。
/Gy 开关不正确使用时的问题是使得联接器为每个函数都分配一个偏移。我已经见到很多有几千个函数而且使用/Gy 的可执行文件。既然他们使用缺省的16字节偏移量,里面就到处分布有共8KB的INT 3。等一下,还不止这些!你不会想到即使你不打开/Gy开关,如果你用/O1(优化长度)或/O2(优化速度)开关,它也会隐含地打开。
嗨,这看起来有点混乱。我一边既叫你优化长度,另一边又告诉你优化长度会打开这个浪费空间的/Gy。想想长度,又想想速度:你该怎么办呢?
如果你不想强迫编译器使用一个特别的偏移量,至少你还可以用一个开关来解决:/Os。这个开关的意思是"长度重于速度",它将迫使编译器使用1字节的偏移量,而不是缺省的16字节。猜一猜会发生什么事:当你用优化长度的/O1时,/Os开关自动打开,于是虽然/O1也打开了/Gy, /Os会设定偏移为1,于是去除量/Gy通常会带来的多余的长度。相反,如果你用优化速度的/O2,/Os不打开,于是你就有了垃圾。所以:用/O1而不用/O2,至少在微软提供更好的办法之前如此。

没有下一页了。因为我的时间关系,拜托朋友老狐狸继续翻译,所以请您到老狐狸的主页http://kingfox.163.net 上去继续翻阅吧。
松鼠拜上。