更好的代码优化
一个好的软件开发者总会想方设法提高软件的执行效率,编译器的编写者是一种特殊类型的开发者,不仅代码要执行效率高,而且由它们生成的代码也必须极有效率。因此,任何一个成功的编译器产品,优秀的后台优化是必不可少的。而在这方面,Visual C++ 2005脱颖而出。
Visual Studio .NET 2002和Visual Studio .NET 2003在C++编译器中引入了一些非常好的优化方式,也花了很大气力改善本地代码的执行效率,加入了对Intel Pentium 4 CPU的SSE和SSE2指令支持。特别值得一提的是,还加入了全局程序优化WPO(Whole Program Optimization),可允许链接器在链接.obj文件时,对整个程序进行优化。这些.obj文件与一般.obj文件有所不同,因为它们不但包含了本地机器码,而且还包含了一些中间语言数据,以便编译器的前端和后台之间进行沟通。链接器可把这些文件当成一个大的整体单元来优化,生成更多的内联函数,进行更好的堆栈对齐,还可在多种情况下,使用定制的函数调用约定。Visual C++ 2005在基于自上而下、从底至上的程序结构分析基础上,在WPO上进行了改进,使之更进一步,而最大的改进之处就是配置向导优化PGO(Profile Guided Optimization)了。
对源代码的静态分析,仍留给了编译器许多未解决的问题。就拿对两个变量的比较语句来说,第一个通常比第二个大吗?在switch语句中,哪一个case子句是经常被执行的呢?哪个函数是经常被调用的,而又哪些代码是“冷代码”——即不经常执行的呢?如果编译器在编译时就能知道代码在运行时的状态,就能进行更好的优化,这就是Visual C++ 2005编译器改进的着力之处。
图4:配置向导优化
图4图示了PGO的编译流程,第一步是编译代码,并把它们链接成由一系列配置计数探测数据组成的配置文件。在WPO下,编译器生成的.obj文件不再包含本地机器码,而由中间语言数据组成。这些计数数据由两部分组成:数值计数与命中计数;数值计数常用来表示变量数值的柱状图,而命中计数用来跟踪程序中的特定代码区域被执行了多少次。先运行一个应用程序,再进行一些通常的操作,就能从这些计数中收集相应的数据,并写入一个配置数据库中。当原始的.obj文件被送往链接器时,配置数据也同时被送回链接器,此时链接器就可以进行分析,以决定采取怎样的优化,并最终生成一个不含配置信息的程序,而此最终版本就可发布给用户使用。
配置向导优化可进行多种多样的优化。基于命中计数,能在每个调用点都决定是否采用内联函数;而数值计数,可使switch和if-else结构重新排列,以便找出最常用的数值,从而避免不必要的检查。代码段也能被重新排列,使最常用的代码能一直执行,而不是强制一些不必要的跳转,从而避免TLB(Translation Lookaside Buffer)发生颠簸和页面调度。
“冷代码”被编译器放置在模块的特定区,以避免上述情况的发生;在某一特定类型的虚拟调用点上,虚拟调用推测能避免vtable查找;局部内联可对“热代码”进行内联化处理。另外,代码的特定区域也能有针对性地进行某种优化,而其他区域进行另外某种优化,例如,“热代码”或小型函数能被指定编译为最快速度(/O2),而“冷代码”或大型函数能被指定编译为占用最小空间(/O1)。
如果十分清楚程序运行的真实情况,可在配置文件生成时,不断地在此模拟情况下运行程序,而最终的程序执行效率将会得到极大的提升。最近,SQL Server使用PGO重新编译,结果在多数应用环境下,可得到最高30%的效率提升;由此看来,微软会使用此技术来编译它的全部产品。要注意的是,不要在配置文件生成时,试图进行完全代码路径覆盖,PGO的中心点是针对普通使用情况,来决定是否优化,如果试图进行完全代码路径覆盖,只会自食其果。
Visual C++ 2005也加入了对OpenMP的支持,OpenMP是一个用于创建多线程程序的开放规范,它由一组pragma组成,指示编译器可把某段代码进行并行处理。不依赖前一个迭代结果的大循环代码就非常适合OpenMP,请看下面简单的拷贝函数,它把数组a和b中的数值相加,存放于数组c中:
void copy(int a[], int b[], int c[], int length) { #pragma omp parallel for(int i=0; i<length; i++) { c[i] = a[i] + b[i]; } } |
在多处理器电脑上,编译器将生成多线程来执行此循环的迭代,每个线程都会执行拷贝操作的一个子集。需注意,编译器不会去检查循环是否存在依赖性,因而甚至不会阻止你在一此不适合的情况下使用pragma。如果存在依赖,即使程序对规范而言是正确的,也会得到与预期相反的结果。
虽然OpenMP的最大好处是并行执行如上所示的循环,但顺序代码也能从中得到性能上的提高,“#pragma omp section”可被直接用于区分代码中的非依赖区,允许开发者指定可并行执行的区域,接下来,编译器可生成多线程代码,以在不同的处理器上执行这些代码段。
对使用 .NET的开发者来说,最重要的一个变化就是,当目标平台为MSIL时,编译器会像对待本地代码平台一样,进行绝大多数都相同的优化。虽然现今的JIT即时编译器是在运行时为优化进行分析,但允许C++编译器在初始编译期间进行优化,仍能产生可观的优化效果(相对JIT即时编译器,C++编译器有更多的时间进行它的分析)。Visual C++ 2005是首次对托管类型进行优化,包括循环优化、表达式优化、内联优化,而通常这些是编译器不能进行 .NET代码优化的地方。例如,因为指针算法的不可验证性,将导致强度消减问题;又因为CLR的严格类型和成员访问需要,某些代码可能不会被内联化。另外,优化MSIL也要根据即时编译器所面对的代码,作出一个平衡,举例来说,你可能不想打开一个循环,并把过多的变量暴露给即时编译器,因此,它就必须进行寄存器分配(一个NP-complete问题)。