经验小结

1. 好的算法

  • 新的 macro 替换算法,去掉 hideset(极为影响性能)

hideset 来自算法:C macro expansion algorithm

这个算法虽然完备,但是性能不佳,而且也不是最简单的。新版的算法参考了 GCC。

  • IF/IFDEF/IFNDEF 条件指令的 skipping 处理

  • 去掉 vector:不必要的封装拖慢速度

  • 简单有效的 hash 算法:去掉速度慢的 FNV 算法

2. 重复使用频繁分配和销毁的内存

  • 比如 ifstack, macro context 以及 macro 展开后的 token 指针数组

  • 新增 cpp_buff:对性能提升明显

3. 对经常分配的数据结构进行预分配

如 token, macro 等。

4. 速度慢的操作延迟加载

一些涉及到系统的库函数调用如 time, localtime, setlocale 比较耗时,延迟到第一次使用时再调用。

5. 延迟预处理阶段不需要的操作

比如常量的求值,预处理阶段只有条件指令需要这样做。去除不必要的常量求值还能减少 token 大小,因为不用在 token 里保存 value 了。

6. 针对多数情况进行优化

例如针对多数头文件都被一个 #IFNDEF 条件指令包围的情况优化,避免多次读入同一个头文件,减少文件读取和解析次数。

再如词法分析中多数注释中带有「fast path」之类的专门优化。

7. 数量巨大的 token 如空白、换行,作为标志位而不是单独创建 token

8. token 数据结构要小

如上面第5点所说,去掉用来保存常量求值结果的 value。

善用 union。

目前的 token 大小是32字节,GCC 和 clang 都是24字节。差距在于表示 token 的 source location 字段,目前是16字节,而 GCC 和 clang 都使用了编码,4字节。

9. 善用位标志

对于多个 boolean 类型的状态,使用同一个整数 flags 表示,节省内存。

10. strbuf 的改造

不在堆上分配 strbuf 结构体本身,而是在栈上使用结构体,而其内容才分配在堆上。

11. 耦合部分用回调

比如错误诊断函数,需要和 cc 共用全局变量 errors, warnings,用回调解决。

12. 预处理输出:fputs 取代 printf/fprintf

由于不用格式化,速度明显提升。

13. 开启 -O2 编译选项

GCC 编译自身也开启了 -O2 选项。

14. 性能调试工具 gprof

性能调优不能靠猜,需要找到专门的工具来找到瓶颈在哪里。gprof 虽然不够完美(比如不那么精确,每次都不一样,再如对子进程的处理很麻烦),但也可以作为参考,达到目的。

总结展望

经过重写优化后,预处理速度大幅地提升,已经比 GCC 和 clang 要快,略逊于 tcc,但 tcc 的快是以丢失信息为代价的。GCC 和 clang 对比,可能是由于 clang 使用 C++ 编写的缘故,速度要不如 GCC。

目前还存在一些问题未处理,因此离一个可称为产品级别的 Release 还有差距:

  1. macro 展开后的 source location 更新,但这会降低性能,有必要对 source location 进行编码成4字节以提升性能。
  2. 如果要作为一个 stand-alone 的 CPP,还需要处理 avoid-paste 问题