找回密码
 立即注册
查看: 531|回复: 0

[笔记] 使用 Unity Build 加速 CMake 编译

[复制链接]
发表于 2020-11-25 09:09 | 显示全部楼层 |阅读模式
本文使用 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>吧。
四、参考代码
  1. # Unity Build 函数,将输入的文件列表打包输出为一个 unit_build.cpp文件
  2. # input_src:文件列表,调用时通过字符串传递,如 "${PROJECT_SRC}"。若在.cmake文件中调用,则需要提供绝对路径
  3. # output_file:输出文件,存放在 ${CMAKE_BINARY_DIR}/${output_file},即build目录
  4. function(UNITY_BUILD output_file input_src)
  5.     file(WRITE ${CMAKE_BINARY_DIR}/${output_file} "//  ${output_file}\n")
  6.     foreach(filename ${input_src})
  7.         file(APPEND ${CMAKE_BINARY_DIR}/${output_file} "#include \"${filename}\"\n")
  8.     endforeach()
  9.     message("Generate unity build file: ${CMAKE_BINARY_DIR}/${output_file}")
  10. endfunction(UNITY_BUILD)
  11. # 使用范例
  12. set(MODULE_SRC
  13.     a.cpp
  14.     b.cpp
  15.     c.cpp
  16. )
  17. UNITY_BUILD(module_unity_build.cpp "${MODULE_SRC}")
  18. add_executable(${PROJECT_NAME}
  19.     main.cpp
  20.     ${CMAKE_BINARY_DIR}/module_unity_build.cpp
  21. )
复制代码
注意:
    谨慎使用全局变量。不同源文件的全局变量可能存在重名,导致重定义错误。可以考虑把全局变量改为类静态成员。尽量规避宏定义。宏定义为全局最高优先级替换,在 Unity Build 中,可能会破坏其他源文件中的标识符,导致编译错误(说的就是你俩,windows.h的max和,xlib.h的Status!)。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-2 11:40 , Processed in 0.583924 second(s), 29 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表