tcc -O2会做什么?

先前在知乎看到有人发问题,问为什么TCC比GCC、Clang快那么多:对于题主生成的一个很大的源码文件,Clang编译它直接崩溃,GCC花了很长时间,而TCC很快就编译完了。最有趣的一点是,题主在给出自己使用方式的例子时,对比的是:
$ clang -c -O2
$ gcc -c -O2
$ tcc -c -O2

就在专栏这边记录点文字吧。

TinyCC(Tiny C Compiler),简称TCC,它自身体积非常,编译/链接速度非常,生成的代码质量一般,可以自举(自己可以编译自己)。以前写了点简单的介绍,传送门:Tiny C Compiler是个怎样的编译器?有人更新吗? - RednaxelaFX的回答 - 知乎

之前也提到了,TCC之所以小而快,主要是因为它采用了典型的单趟编译器(one-pass compiler)常见的“语法制导翻译”方式来贯穿整个编译器的实现。简单来说,整个编译器都融合在语法分析之中——由语法分析来驱动预处理与词法分析,同时由语法分析来驱动语义分析与目标代码生成;在完成语法分析的流程后,最终的目标代码也就生成完毕了,中间不构造代码的任何中间表示(IR),连AST也不构造。

这种做法在现代优化编译器中已经很少见,但在一些极度精简的编译器或者脚本语言实现中的前端(例如Lua)中还很常见。这是因为这种架构常常需要把太多东西塞在一起,既不便于理解也不便于调试,同时也非常难扩展。

它也限制了信息流动的方向,使得某些需要后向数据流分析(backward data-flow analysis)的优化都无法实现,例如活跃变量分析(liveness analysis)/无用代码删除(dead code elimination)都做不了。

所以TCC实际实现了一些怎样的优化呢?官网文档里也有写到,它实现了简单的常量传播与折叠(都是前向数据流(forward data-flow));还实现了某些运算的强度减弱(strength reduction)优化,例如把2的幂方的常量乘法生成为左位移之类。也就这样了。TCC所实现的这些优化都是不可以用参数来控制的。

但TCC确实支持-O<n>选项,传-O0、-O1、-O2、-O3…-O10000给它,它都可以接受。那么tcc -O<n>到底干了什么?

答案是:tcc -O,其中n大于0的时候,TCC会给被编译的程序定义 __OPTIMIZE__ 宏。就跟 -D__OPTIMIZE__ 一样。相关实现在这里:libtcc.c

就这样。嗯。-O1,-O2,-O10000都一样。

TCC本身预处理/词法分析/语法分析就写得很紧凑,外加不用生成AST或者任何其它形式的IR,基本上同样的源码输入进来,GCC还没完成语法分析+GENERIC形式的AST构建,TCC就已经完成目标代码生成,可以收工了…然后GCC在-O2下要得做些耗时间及内存的分析优化,那编译速度自然是不能比咯——生成的代码质量也同样不能比。

就拿下面这个文件来演示一下吧:

int const_fold_literals() {
  return 40 + 2;         // both tcc and gcc optimized to 42
}

int const_fold_literals_across(int x, int y) {
  return 40 + x + y + 2; // tcc -O2: not optimized
                         // gcc -O2: optimized to 'x + y + 42'
}

int cond_const_prop(int a) {
  if (a) a = 0;          // tcc -O2: not optimized
                         // gcc -O2: optimized to 'return 0'
  return a;
}

int main() {
  const_fold_literals();
  const_fold_literals_across(2, 3);
  cond_const_prop(0);
  return 0;
}

嗯…


=========================================

这个 __OPTIMIZE__ 宏是一个GCC扩展里定义的宏,Clang也支持它。它的作用是让某些库函数可以根据程序编译时是否优化来选择不同的实现。例如说glibc的string.h

#ifdef __OPTIMIZE__
__extern_always_inline char *
strstr (char *__haystack, const char *__needle) __THROW
{
  return __builtin_strstr (__haystack, __needle);
}

__extern_always_inline const char *
strstr (const char *__haystack, const char *__needle) __THROW
{
  return __builtin_strstr (__haystack, __needle);
}
# endif
}
#else
extern char *strstr (const char *__haystack, const char *__needle)
     __THROW __attribute_pure__ __nonnull ((1, 2));
#endif

这个宏其实自身并不影响编译器的优化程度。不过当有些标准库函数有编译器intrinsics的时候,通过这个宏来保证库里实现功能的代码跟编译器intrinsics要表达的语义完全一致,是件好事。

18 条评论