编译器开发一直是计算机科学中的基础性重点研究领域以至于国内每本计算机导论类的教材都将其归为和OS一级的系统软件,所以经过几十年的发展编译原理有一整套完善详尽的理论。在科学领域追求真理的道路上不存在终点,有的只是一代代开拓者们对精益求精的诠释。
编译器对程序的优化有三条基本原则:
(1)等价原则。经过优化后不应改变程序运行的结果;
(2)有效原则。使优化后所产生的目标代码运行时间较短,占用的存储空间较小;
(3)合算原则。应尽可能以较低的代价取得较好的优化效果。
第一条是最基本的也很好理解,给出相同输入经过优化前后的程序应该得出相同的输出,否则程序运行的结果都不确定了这种编译器还有人敢用吗?第二条也容易理解,可以理解为程序优化的目的。第三条对C/C++这种静态语言来说约束不是很强,静态语言的一大特性就是以编译时间换取运行效率;但对于像Lua、Python这种动态语言来讲就不可能花费三到五倍于编译的时间在优化上,因为这类动态语言大多以源代码为最终程序并且在初始化期间要完成加载、解析、编译等一系列过程。
编写程序一些优化点我是一直信任编译器不做手工优化而只考虑代码的直观,我一般是不会主动手工写出代码外提、强度销弱这种代码的。但前两周的一个程序bug让我看到了编译器优化的陷阱。情况是这样的,在我的工程里有若干[字符串-函数指针]对,每个字符串都不同,函数指针之间也没有重复,程序分为解析和运行两个阶段,解析阶段会根据读到的字符串取相应的函数指针,运行阶段为了优化速度会对函数指针进行判断而不做字符串比较以确定某个[字符串-函数指针]对的内容,函数指针所指向的函数有些实现了相应的功能,直接可以用函数指针调用具体功能函数,另外有一部分函数不做任何实际功能,只是为了运行期比较的高效而存在。写个简化代码,为了简洁不使用[字符串-函数指针]对只针对将要出问题的函数指针写出程序:
1 void dummy_fun1(void) {
2 }
3
4 void dummy_fun2(void) {
5 }
6
7 typedef void (*func_t)(void);
8
9 int main() {
10func_t f1 = dummy_fun1;
11func_t f2 = dummy_fun2;
12if(f1 == f2) {
13printf("Oops!\n");
14} else {
15printf("Fine.\n");
16}
17
18return 0;
19 }
猜猜程序编译运行会输出什么?在“启用COMDAT折叠”为“默认值”的Debug配置项下会输出“Fine”,而在为“移除冗余的COMDAT”的“Release”配置项下会输出“Oops”。问题出现了,编译器擅自使用了删除冗余代码的优化方式将dummy_fun1和dummy_fun2编译链接为了同一个函数而不是两个具有不同入口地址的空函数。给出相同输入在经过优化前后的程序时得到了不同的结果,这直接违反了编译器优化的等价原则,我是无论如何也不能同意我所使用的编译器有这种行为的。如果我来设计编译器在符号依赖性检查时会发现dummy_fun1和dummy_fun2的地址被放到了f1、f2两个变量里,并且这两个变量有比较操作,那么对于任何其地址有可能参与运算的等效函数优化为同一个。
知道了问题的原因解决办法也很好写出来,只要让编译器认为这是两个不同函数就行了,最简单的添加下面的宏并在每个要区别开的函数里写上该宏:
1 #define do_nothing { \
2 assert("Unaccessable function"); \
3 printf("%s\n", __FUNCTION__); \
4 }
5
6 void dummy_fun1(void) {
7 do_nothing;
8 }
9
10 void dummy_fun2(void) {
11do_nothing;
12 }
13
14 typedef void (*func_t)(void);
15
16 int main() {
17func_t f1 = dummy_fun1;
18func_t f2 = dummy_fun2;
19if(f1 == f2) {
20printf("Oops!\n");
21} else {
22printf("Fine.\n");
23}
24
25return 0;
26 }
最后推荐一本讲解编译器优化的好书《高级编译器设计与实现》,(美)马其尼克 著,赵克佳,沈志宇 译,机械工业出版社。