利用pre-compiled headers技术以加速编译速度
--以Borland C++ Builder为例
(三)
本文作者:王森
台湾交通大学科技管理研究所
moli.mt88g@nctu.edu.tw
<pre-compiled headers技术的运作方式>
在前面一个段落中我们经由推论得到一个假设,接下来我们要证明我们的推论是否正确。因此我们开始修改Unit1.cpp以及Unit2.cpp,如下:
程序代码5:
Unit1.cpp
#include <iostream.h>
#include <stdio.h>
#include <vcl.h>
#pragma hdrstop
#pragma argsused
int main(int argc, char* argv[])
{
cout << "Hello World" ;
return 0;
}
Unit2.cpp
#include <iostream.h>
#include <stdio.h>
#include <vcl.h>
#pragma hdrstop
#include "Unit2.h"
void test(void)
{
printf("test") ;
}
也就是说,我们试着让两个档案有相同的『预先编译标记』。
这次的测试结果为:
测试结果7:
编译次数
编译行数
编译时间
第一次(build)
202250
9.18
本次测试所依据的程序代码和测试结果4所依据的程序代码完全相同,除了编译器指令#pragma hdrstop之前所引入的标头档我们把他改成两个档案皆相同。将这个测试结果拿来与测试结果4比较,编译行数少了快一半。
我们再观察BCB所在目录之下的lib目录,此时会发现之前的测试只有两个档案,他们分别是: vcl50.csm、vcl50.#00。 嘿嘿~ 果然如我们所料,编译器并没有笨到为每个使用编译器指令#pragma hdrstop的档案都各自产生一个cache档,而是一个Project之中,有多少种『预先编译标记』,编译器就产生多少个vcl50.#??的档案,这样一来可以有效减少存放这些cache档所需要花费的空间。
经由上面这个实验证明了我们之前的假设是正确的。但是,前面有一句话:『编译器会为每个使用编译器指令#pragma hdrstop的档案产生一个cache文件,以加速编译』这句话要修正成为『编译器会为Project之中各种不同的预先编译标记产生不同的cache文件,以加速编译』。
至于预先编译标记是以何种方式来记录呢?个人猜想只有四种方法较可能:
1. 利用registry
2. 编译器内部的数据结构
3. 产生预先编译标记数据库
4. 直接纪录在vcl50.#??
大家应该记得我们在做以上实验的时候,都可以自己随心所欲地删除那些cache 档,因此,个人认为1、2的做法比较不可能,而3、4是比较可行的方法。笔者自行用工具检测每次编译后registry是否有改变,结果发现registry并没有因此而改变;如果是使用编译器内部的数据结构,那势必只能针对每一个Project建构预先编译信息,可是后来笔者自己做了实验后发现,这些预先编译的cache檔可以”跨Project”使用,也就是说不管是否存在同一个Project之中,这些cache档是可以让每个Project共享,编译器只关心预先编译标记是否存在。然而当笔者自己使用UltraEdit开启vcl50.csm以及vcl50.#??的时候,笔者利用UltraEdit的搜寻功能去搜寻这两个档案里面是否有类似 iostream 或是stdio 等字符串之后,大致上可以推论出vcl50.csm比较类似的3的做法,而vcl50.#??比较类似4的做法,或许Borland的工程师两种方法都有用上吧! 由于欠缺更详尽的资料,所以无法再做更详尽的剖析,如果有读者研究出结果,别忘了告诉大家喔!
最后,我们把编译器在编译原始码时,编译器所采用的编译逻辑画成下面这张流程图:
<预先编译标记是怎幺回事?>
前一段我们经由实验证明了的确存在有预先编译标记这个抽象的概念,可是预先编译标记是根据什幺原则所产生的呢? 前面我们做的假设 -- 编译器指令#pragma hdrstop之前所有引入的标头档档名所构成的集合称做『预先编译标记』,真的是如此吗? 让我们再做几个小实验来证明看看是否是我们假设的那样。
我们分别修改程序代码5的内容,把编译器指令#pragma hdrstop之前所引入的标头档做下面的调整:
调整一 : 让标头档档名的大小写不同
Unit1.cpp
Unit1.cpp
#include <iostream.h>
#include <stdio.h>
#include <vcl.h>
#pragma hdrstop
#include <IOstream.h>
#include <stdio.h>
#include <vcl.h>
#pragma hdrstop
◎测试结果è
编译次数
编译行数
编译时间
第一次(build)
404451
12.92
◎产生的cache檔è
vcl50.csm、vcl50.#00、vcl50.#01
调整二 : 让标头文件排列顺序不同
Unit1.cpp
Unit1.cpp
#include <iostream.h>
#include <stdio.h>
#include <vcl.h>
#pragma hdrstop
#include <stdio.h>
#include <iostream.h>
#include <vcl.h>
#pragma hdrstop
◎测试结果è
编译次数
编译行数
编译时间
第一次(build)
404451
11.12
◎产生的cache檔è
vcl50.csm、vcl50.#00、vcl50.#01
调整三 : 让标头档档名之间有空格
Unit1.cpp
Unit1.cpp
#include <iostream.h>
#include <stdio.h>
#include <vcl.h>
#pragma hdrstop
#include <iostream.h>
#include <stdio.h>
#include <vcl.h>
#pragma hdrstop
◎测试结果è
编译次数
编译行数
编译时间
第一次(build)
202251
5.40
◎产生的cache檔è
vcl50.csm、vcl50.#00
从上面的三种调整程序代码后所测得的结论,我们可以得到以下结论:
预先编译标记是由编译器指令#pragma hdrstop之前所引入的标头所决定的,而且两个程序原始文件要构成”预先编译标记相同”的条件是:
1. 引入的标头档要完全相同
2. 标头文件的排列顺序要正确
3. 若有使用编译器指令#define,其内容跟顺序也都要相同(由2导出)
4. 标头档档名的大小写也要一致(case-sensitive)
5. 至于空白列存在与否,对预先编译标记并不构成影响。
总而言之,就是除了空白行之外,每个档案的#pragma hdrstop之前都要长的一模一样才行,有一点不同就会造成预先编译标记的不同。
所以我们可以说:如果要让pre-compiled heads技术发挥到最极限,则我们应该让程序中的每一个程序原始文件都引入相同的标头档,即使该标头档里面的函式在该程序原始文件之中没有用到也要引入。喔喔! 接下来一定会有人跟我抱怨:『那我的Project里头有100个程序原始文件,每次我的任何一个程序原始文件里面新引入了一个标头档,那幺我就要修正其它99个档案,让每个档案引入的标头档一致,那岂不是累坏了?』。由这个问题之中,我们引出了最佳解法,这个方法同时也可以加速我们在使用build(重头到尾重新编译)时,加速编译速度的方法,就是:『把所有的 #include指令全部都搬到一个单一的标头档里面,然后Project里面的每个档案都直接引入这个单一的标头档』,比方说我们可以在Project之中新增一个独立的标头档,叫做includeall.h,这个档案的内容如下:
includeall.h
#include <iostream.h>
#include <stdio.h>
#include <vcl.h>
然后把Unit1.cpp与Unit2.cpp各自修改成:
Unit1.cpp
#include “includeall.h”
#pragma hdrstop
#pragma argsused
int main(int argc, char* argv[])
{
cout << "Hello World" ;
return 0;
}
Unit2.cpp
#include “includeall.h”
#pragma hdrstop
#include "Unit2.h"
void test(void)
{
printf("test") ;
}
之后,只要任何时候某个程序原始文件需要引入某个标头档的时候,就一律修改includeall.h就可以了,这样一来,就可以让所有的程序原始文件都保有相同的预先编译标记,让pre-compiled headers技术发挥到最极限,而程序设计师也可以省下许多同步更新每个程序原始文件的时间。
当然还有更偷懒的人会问: 『为何不在includeall.h之中直接把#pragma hdrstop写进去呢?这样不是我们可以在其它每一个程序原始文件里面都少打这一行呀?』这个问题问的很好,笔者也是这样一个懒惰的工程师,不过实际上试过之后,发现这样子做是有问题的。编译器的确有使用pre-compiled headers技术,但是,编译器把所有的程序原始码都看做有有不同的预先编译标记,也就是说,如果Project之中有40个档案都引入includeall.h,那幺就会产生40个cache,而且大小全部相同,这幺一来不但没有让编译速度加快(因为几乎每个程序原始文件都是重新彻底编译),而且还因为要产生那幺多cache档而浪费了整体时间,也浪费了硬盘空间,所请请读者千万不要把#pragma hdrstop写到includeall.h里面。
至于为何会发生这样的事情呢?终于让笔者在BCB的on-line help在解说#pragma hdrstop的地方找到下面这行:
“Use this pragma directive only in source files. The pragma has no effect when it is used in a header file.”
虽然没有提到会有反效果,但是上面这行至少说明了把编译器指令#pragma hdrstop放在标头档之中是没有效果的(就算我们把附档名从.h改成.cpp也没有用)。
呼~ 为了得到这个结果,花了我们好大的功夫呀!!