include-what-you-use(以下简称IWYU)是Google推出的用来检查头文件冗余的工具。原理是分辨出#includes里有哪些是当前文件(.cpp,.cc,.h)完全没有用到的,以及使用前置声明来替代#includes。线上的大型项目中经过一段时间的线上开发过程,经常会有间接引用的情况,例如a.h实际是需要include c.h,但是已经include b.h中已经include c.h了,这样虽然编译能通过,但是增加了耦合,不利于后续的维护。
IWYU的好处
- 更快的编译。 当文件包含冗余的头文件时,编译器会读取,预处理和解析更多的代码。如果存在模板,则会引入更多的代码,加大编译时间。 使用前置声明代替include语句,也能减少依赖,减少可执行程序的大小。
- 更好的重构。 间接引用的存在会导致去除include时,编译失败。不利于解耦。
- 头文件自注释。可通过查看头文件注释,知道该功能依赖其他哪些子功能。
include-what-you-use是使用clang分析符号的引用,每个clang版本更新时,iwyu都会有对应的改动。如果开发环境下已经安装了clang的环境,那么需要对照版本下载对应的iwyu的版本。下面是官网(https://include-what-you-use.org/) 提供的版本对应表。github的地址:https://github.com/include-what-you-use/include-what-you-use
Clang | IWYU version | IWYU branch |
---|---|---|
3.6 | 0.4 | clang_3.6 |
3.7 | 0.5 | clang_3.7 |
3.8 | 0.6 | clang_3.8 |
3.9 | 0.7 | clang_3.9 |
4.0 | 0.8 | clang_4.0-r2 |
5.0 | 0.9 | clang_5.0 |
6 | 0.10 | clang_6.0 |
7 | 0.11 | clang_7.0 |
8 | 0.12 | clang_8.0 |
9 | 0.13 | clang_9.0 |
10 | 0.14 | clang_10 |
11 | 0.15 | clang_11 |
... | ... | ... |
master | master |
如果项目没有clang环境,那么首先要安装llvm框架系统,然后选择对应的iwyu安装。这里不介绍llvm的安装方法了。
IWYU安装
下载好iwyu源码后。解压到目录下iwyu下。使用cmake指定好llvm的路径,和源代码的路径,编译项目。
#拉取IWYU源码
iwyu$ git clone https://github.com/include-what-you-use/include-what-you-use.git
#进入目录 切换到llvm版本对应的IWYU分支
iwyu$ cd include-what-you-use
iwyu/include-what-you-use$ git checkout clang_11
#返回上级目录 准备cmake相关
iwyu/include-what-you-use$ cd ..
iwyu$ mkdir build && cd build
#IWYU 0.10/CLANG 6 或者更早的版本
iwyu/build$ cmake -G "Unix Makefiles" -DIWYU_LLVM_ROOT_PATH=/usr/lib/llvm-6.0 ../include-what-you-use
#IWYU 0.11/CLANG 7 或者更新的版本
iwyu/build$ cmake -G "Unix Makefiles" -DCMAKE_PREFIX_PATH=/usr/lib/llvm-7 ../include-what-you-use
#cmake生成好编译文件后
iwyu/build$ make -j8
#安装iwyu
iwyu/build$ sudo make install
安装完成后,默认是安装到了/usr/local/bin/目录下。
IWYU使用
编译项目时,如果是使用Makefile,插入-k CXX=/path/include-what-you-use即可。
下面是一个使用Makefile的简单例子
test_sample.cpp
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <iostream>
int main(int argc, char* argv[])
{
std::string tmpstr = "test";
std::cout << tmpstr << std::endl;
return 0;
}
Makefile
CXX=/usr/bin/clang++
TARGET = test
OBJS = test_sample.o
$(TARGET) : $(OBJS)
@rm -rf $(TARGET)
@$(CXX) $(OBJS) -o $@
clean:
@rm -rf $(TARGET) $(OBJS)
.PHONY: clean
直接输入
$ make -k CXX=/usr/local/bin/include-what-you-use
输出结果
test_sample.cpp should add these lines:
#include <string> // for operator<<, string
test_sample.cpp should remove these lines:
- #include <math.h> // lines 3-3
- #include <stdio.h> // lines 1-1
- #include <string.h> // lines 2-2
The full include-list for test_sample.cpp:
#include <iostream> // for endl, basic_ostream, cout, ostream
#include <string> // for operator<<, string
---
make: *** [<builtin>: test_sample.o] Error 6
make: Target 'test' not remade because of errors.
可以看到IWYU提供了include的一些意见,可以把输出导入到iwyu.out文件中,使用fix_includes.py修改对应代码。
$ make -k CXX=/usr/local/bin/include-what-you-use 2> iwyu.out
$ python /usr/local/bin/fix_includes.py < iwyu.out
修改完成后的代码
#include <iostream>
#include <string>
int main(int argc, char* argv[])
{
std::string tmpstr = "test";
std::cout << tmpstr << std::endl;
return 0;
}
可以看到includes已经被自动应用修改了。
fix_includes.py --help可以查看一些额外的参数,例如默认是不会移除没有使用的includes,加上--nosafe_headers,可以在.h头文件中移除iwyu认为多余的includes。 --comments可以给includes加上注释,表明用到了其中的哪个模块。
如果使用的是cmake,在build目录下
mkdir build && cd build
CC="clang" CXX="clang++" cmake -DCMAKE_CXX_INCLUDE_WHAT_YOU_USE="path/bin/include-what-you-use"
make -j8
实际项目使用
在项目中实际使用中,因为项目是使用make编译生成多个进程的,实际优化中是先删除某个进程的编译产生的文件,对不同进程分别使用iwyu。
src$ rm bin/GatewayServer && rm -rf .obj/GatewayServer/
src$ make GatewayServer -j32 -k CXX=/thirdpart/bin/include-what-you-use 2> iwyu_gate.out
src$ cd GatewayServer
src/GatewayServer$ python /thirdpart/bin/fix_includes.py --nosafe_headers < ../iwyu_gate.out
应用完修改后,在对GatewayServer重新编译,因为带了--nosafe_headers,有些间接引用会导致编译无法通过,需要自己手动修改。修改完成后,使用du命令查看makefile生成的中间文件大小。 .obj/GatewayServer/的大小从32248kb缩小到了32192kb,减少了50kb左右的大小。使用/usr/bin/time对比单线程的 前后的编译时间差距,User Time从90.37s缩短到了88.94s,System Time从8.99s缩短到了8.74s。Real Time从99.35s缩短到97.68s,测试结果说明实际项目中,IWYU是有效果的,但是整体优化效果只能说是一般。整体编译优化上,整个服务器所有进程在使用了多线程编译后,全量编译下,make -j32,测试下来从8m12s缩短到了7min59s。
项目中的另外两个进程Scene和Function使用了Unity Builds,将.cpp文件包含在了多个NXProjectUtil.cpp文件中,使用IWYU分析这部分代码时发现由于使用了Unity Builds,IWYU不能正常工作。修改了Makefile,删除NXProjectUtil.cpp,改为对所有文件编译后,发现由于之前一直是使用Unity Builds导致了很多cpp文件并没有include所需要的头文件,出现了一大堆编译报错。只好对着一堆编译报错慢慢修复,修复完成后再使用IWYU分析代码,发现可以正常工作了。使用IWYU修改完代码后,在回退掉Makefile,恢复移除的NXProjectUtil.cpp,完成了Unity Builds的IWYU代码分析。
Unity Builds是将项目中的所有源代码include到了一个文件,对这个文件编译即可,实际应用中考虑到make是可以多任务执行的,一般是依据模块分成多个文件,由于是对unity.cpp进行编译,可以预见到IWYU对include优化能起到的作用有限,但是长久的维护Unity Builds的项目会导致大量的本应该include的,没有加进去,后续在做IWYU优化十分痛苦。但是也并不是完全没有必要,一般的项目用到Unity Builds还是会分成多个unity文件,这时候对不同unity文件间做解耦还是有必要的。尤其是对后面如果要做重构工作,会轻松很多。
IWYU在使用前置声明替代include时,发现对项目中使用的proto,进行了大量替换,将proto生成的.pb.h文件在.h中全部替换成了前置声明,这里可以预见到对于修改proto导致的增量编译,能起到很好的优化效果。这里测试了修改某个proto文件,进行了一次增量编译测试,使用make -j8。 User Time从1515.30s缩减到1392.49s,System Time从81.83s缩减到76.72s,Real Time从4min46s缩减到4min31s。在增量编译中,可以观察到修改了proto,各个进程的链接过程都是要执行的。但是使用了前置声明的部分,在执行编译时,有部分已经解耦的文件不再做增量编译了,减少了增量编译的时间,可见对日常开发中还是有一定作用的。
参考文档:
https://www.cnblogs.com/cherishui/p/12860452.html
https://github.com/include-what-you-use/include-what-you-use/blob/master/README.md
https://include-what-you-use.org/