cater 发表于 2020-11-25 09:09

使用 Unity Build 加速 CMake 编译

本文使用 Zhihu On VSCode 创作并发布
一、使用效果

未使用 Unity Build

未使用 Unity Build
使用 Unity Build

使用 Unity Build
二、原理说明

C/C++ 的编译系统和其他高级语言存在很大的差异。
其他高级语言中,编译单元是整个module,即module下所有源码,会在同一个编译任务中执行。
而 C/C++ 中,因为历史遗留问题(早年的硬件内存不支持同时加载整个项目的源码进行编译),编译单元是以文件为单位。
即,C/C++ 的编译方式是这样的:
对每个 .c/.cc/.cxx/.cpp 源文件,启动一个独立的编译器进程进行编译,生成 .o/.obj 临时文件;编译完成所有源文件后,使用链接器,将所有临时文件合并链接到目标二进制。
以上编译方式存在几个很严重的性能问题:

[*]每个编译单元,都需要独立解析所有包含的头文件:如果N个源文件引用到了同一个头文件,则这个头文件需要解析N次(对于protobuf这类动辄几千上万行的头文件简直就是鬼故事);如果头文件中有模板(STL/Boost),则该模板在每个cpp文件中使用时都会做一次实例化,N个源文件中的std::vector<int>会实例化N次。
每个源文件独立编译,导致编译优化时只能基于本文件内容进行优化,很难跨编译单元提供代码优化;基于问题2,C/C++ 跨编译单元的优化只能交给链接器,而链接阶段是单进程,无法并行加速,导致大项目链接极慢。
问题1可以通过并行编译解决,但只是治标不治本,总开销依然有大量的资源浪费;也可通过预编译头PCH解决,与本方法差别见后文。
问题2目前暂无解决手段,新标准的module功能有望改善此问题。
问题3可以通过动态库方式降低链接耗时,即拆分为多个二进制目标。(静态库无效,静态库本质上相当于把 .o/.obj 做了一次打包,最后生成二进制时合并到链接器中)
本文提出一个 Unity Build 思路,可以大幅提升编译速度,原理如下:
建立临时文件unity_build.cpp在该文件中,#include包含所有源代码文件(不是包含头文件)不编译分散的源代码文件,而是只编译unity_build.cpp
该方式规避了问题1中的额外开销,对于越复杂的代码(模板/宏用得多),编译速度提升越快。
同时,对于1.2的模板多次实例化问题,Unity Build 只会实例化一次模板,对问题3的链接也有一定加速。
但考虑到多进程并行编译,并不是把所有代码都打包到同一个unity_build.cpp中效果最好。
打包思路可以参考module,即将同一个模块的源代码打包为一个文件,这样该模块中大量公用的头文件就可以避免被多次解析。
三、与 PCH 区别

1、pch不能针对性地对一部分代码生效。比如只想把某三个文件打包编译,pch就不适用。如果专门写个add_library把他们仨单独打包又太麻烦了,还得配置target_include_directories啥的。如果不用target_xxx系列指令的话,又不太符合 target-based 的 Modern CMake 风格,少了内味/Taste。
2、这个方法比pch更激进,相当于把所有头文件全都塞一起了——用pch开发的时候,不可能所有cpp里只有一行#include <stdafx.h>吧。
四、参考代码

# Unity Build 函数,将输入的文件列表打包输出为一个 unit_build.cpp文件
# input_src:文件列表,调用时通过字符串传递,如 "${PROJECT_SRC}"。若在.cmake文件中调用,则需要提供绝对路径
# output_file:输出文件,存放在 ${CMAKE_BINARY_DIR}/${output_file},即build目录
function(UNITY_BUILD output_file input_src)
    file(WRITE ${CMAKE_BINARY_DIR}/${output_file} "//${output_file}\n")
    foreach(filename ${input_src})
      file(APPEND ${CMAKE_BINARY_DIR}/${output_file} "#include \"${filename}\"\n")
    endforeach()
    message("Generate unity build file: ${CMAKE_BINARY_DIR}/${output_file}")
endfunction(UNITY_BUILD)


# 使用范例
set(MODULE_SRC
    a.cpp
    b.cpp
    c.cpp
)
UNITY_BUILD(module_unity_build.cpp "${MODULE_SRC}")
add_executable(${PROJECT_NAME}
    main.cpp
    ${CMAKE_BINARY_DIR}/module_unity_build.cpp
)注意:
谨慎使用全局变量。不同源文件的全局变量可能存在重名,导致重定义错误。可以考虑把全局变量改为类静态成员。尽量规避宏定义。宏定义为全局最高优先级替换,在 Unity Build 中,可能会破坏其他源文件中的标识符,导致编译错误(说的就是你俩,windows.h的max和,xlib.h的Status!)。
页: [1]
查看完整版本: 使用 Unity Build 加速 CMake 编译