背景
本文主要介绍CMake项目,在不改动或较小改动项目代码的情况下,如何减少编译时间。同时简要介绍其原理和使用场景。包含ccache,编译集群,CMake依赖优化,避免动态编译宏和UnityBuild。
加快编译速度
分通解,增量编译和全量编译三个方面来介绍。通解开启简单方便,增量编译减小项目重编范围。全量编译减少编译数量。
通解
使用ccache
原理及使用场景
ccache会缓存编译结果,在编译文件和编译宏未发生变化时,直接返回缓存的编译结果。
使用场景:在文件内容未变化时加速效果近乎100%,开启简单方便。
搭配make使用时,效果有限。
开启方式
使用yum install ccache
安装
在CMake中添加如下内容开启ccache
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
message(STATUS "Found ccache: ${CCACHE_PROGRAM}")
set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_PROGRAM})
set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_PROGRAM})
else()
message(WARNING "ccache not found, proceeding without ccache")
endif()
验证方式
ccache -s
直接查看命中缓存次数。
$ ccache -s
cache directory /root/.ccache
cache hit (direct) 467
cache hit (preprocessed) 2
cache miss 1009
files in cache 1701 这里是缓存命中的次数
cache size 656.0 Mbytes
max cache size 1.0 Gbytes
使用编译集群
原理及使用场景
cpp项目编译时,每个源码文件都是一个编译单元,所以可以使用多线程并行编译。编译集群简单粗暴增加编译的并行数量。
使用场景:大规模编译时效果明显,收费。
开启方式
公司内部devops中就有编译加速,接入很简单。
增量编译
介绍如何减少不必要的编译项目。
如何发现不必要的编译项目?
CMake生成后,使用Make编译时,会打印一些日志,如下所示。
$ make MyExecutable
Scanning dependencies of target MyExecutable
[ 33%] Building CXX object CMakeFiles/MyExecutable.dir/src/main.cpp.o
[ 66%] Building CXX object CMakeFiles/MyExecutable.dir/src/Foo.cpp.o
[100%] Linking CXX executable MyExecutable
[100%] Built target MyExecutable
从日志可以看到在编译MyExecutable时, Make将main.cpp.o和Foo.cpp.o重新编译,说明这两个文件所依赖的文件发生了变化。
将在依赖优化原理处介绍如何查看其依赖内容。这里只需要知道通过日志,可以观察本次编译的内容是否符合预期,如果发现不该编译的文件进行了编译,则可以查看其依赖内容,判断是否可以进行优化。
CMake依赖优化
依赖优化原理
CMake生成之后会生成如下所示的depend.make文件
# depend.make
CMakeFiles/MyExecutable.dir/src/Foo.cpp.o: ../src/Foo.cpp
CMakeFiles/MyExecutable.dir/src/Foo.cpp.o: src/version.h
CMakeFiles/MyExecutable.dir/src/main.cpp.o: ../src/Foo.h
CMakeFiles/MyExecutable.dir/src/main.cpp.o: ../src/main.cpp
depend.make文件中描述了文件间的依赖情况。Foo.cpp.o依赖Foo.cpp和version.h。当version.h的修改时间戳变化时,会导致Foo.cpp.o重新生成。
如不想让冒号左侧的文件重新生成,则需要保证冒号右侧文件的修改时间戳不发生变化。如果内容无变化,但是修改时间戳发生变化,同样会导致左侧文件重新生成。
项目中存在的协议文件,需要转换后生成代码文件才能在代码中使用。如果转换时使用批量处理,会导致协议文件未变化,但代码文件发生了变化。代码文件发生变化后,所有依赖此代码文件的目标都会重新生成。
正确的做法应该是在协议文件发生变化时,再进行代码文件生成。手动识别较繁琐,正好CMake提供了此功能。
使用场景
编译时存在动态生成文件时
弊:需要理清生成文件的依赖关系
开启方式
set(XML_SRCS)
file(GLOB CFG_FILES "*.xml")
set(GameResFile GameRes.h)
# 主要命令,定义了产出文件GameResFile,生成脚本ConvertGameRes.sh,生成GameResFile所需的文件CFG_FILES
add_custom_command(OUTPUT ${GameResFile}
COMMAND bash ConvertGameRes.sh
DEPENDS ${CFG_FILES}
WORKING_DIRECTORY ${RES}/script/one
)
# 将所有动态生成文件添加到一个目标如XmlHeaders中
list(APPEND XML_SRCS ${GameResFile})
add_custom_target(XmlHeaders ALL DEPENDS ${XML_SRCS})
# 需要使用GameRes.h的服务添加XmlHeaders为依赖
add_dependencies(AServer XmlHeaders)
编译AServer时会检查其依赖XmlHeaders是否有变化,XmlHeaders检查XML_SRCS即GameResFile是否需要重新生成。
当GameResFile的依赖文件CFG_FILES发生变化或产物GameResFile不存在时,调用ConvertGameRes.sh进行生成。
避免使用频繁变化的编译宏
原理
在CMake中可以使用add_definitions添加编译宏,之后在代码文件中即可使用此宏获取对应的内容。如下所示打印编译时间。
# 定义BUILD_TIME宏
add_definitions(-DBUILD_TIME=\"${BUILD_TIME}\")
# 使用宏
std::cout << "Current machine IP address: " << BUILD_TIME << std::endl;
编译宏会存储到如下CMake生成的flags.make中
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 3.17
# compile CXX with /usr/bin/c++
CXX_DEFINES = -DBUILD_TIME=123
每当编译宏发生变化时flags.make的修改时间也会发生变化。在build目录中搜索依赖此文件的内容,如下所示。
$ grep -R "flags.make" ./*
./build.make:include CMakeFiles/MyExecutable.dir/flags.make
./build.make:CMakeFiles/MyExecutable.dir/src/main.cpp.o: CMakeFiles/MyExecutable.dir/flags.make
./build.make:CMakeFiles/MyExecutable.dir/src/Foo.cpp.o: CMakeFiles/MyExecutable.dir/flags.make
可以看到main.cpp.o和Foo.cpp.o都依赖了flags.make。
所以每当cmake重新生成,宏内容发生变化时,flags.make也会发生变化,导致依赖flags.make的文件重新生成。
使用场景
项目存在变化的编译宏时使用。
开启方式
// 文件CompileVersion.cpp.in
#define _BUILDDATE "@BUILDDATE@"
#define _BUILDIP "@BUILDIP@"
#include "CompileVersion.h"
std::string GetCompileVersionStr()
{
std::string strVersion;
strVersion += "\033[40;32mSo Build Date \033[0m: "_BUILDDATE"\n";
strVersion += "\033[40;32mSo Build Host \033[0m: "_BUILDIP"\n";
return strVersion;
}
// 文件 CompileVersion.h
#pragma once
#include <string>
std::string GetCompileVersionStr();
// 文件 CMake
configure_file(CompileVersion.cpp.in CompileVersion.cpp)
configure_file会将模板CompileVersion.cpp.in中@BUILDDATE@替换成CMake中BUILDDATE变量。同时将产出放在cpp中,对外提供h文件。这样当版本信息发生变化时仅会编译CompileVersion.cpp
全量编译
使用UnityBuild
原理及使用场景
默认的编译方式会逐个处理每个编译文件,如果A.cpp和B.cpp都include了common.h,编译A.cpp和B.cpp时common.h会被处理两次。将A.cpp和B.cpp合并为一个编译单元后,common.h仅会被处理一次。
弊:命名空间污染,重新编译范围扩大,同时手动组织比自动组织效果更好。
自动组织开启方式 cmake3.16添加
# CMake中打开UnityBuild
SET(CMAKE_UNITY_BUILD OFF)
# 设置成一个文件所需的数量
set(CMAKE_UNITY_BUILD_BATCH_SIZE 16)
# 关闭指定文件的UnityBuild,使其单独编译
set_source_files_properties(A.cpp PROPERTIES SKIP_UNITY_BUILD_INCLUSION ON)
手动组织开启方式 cmake3.18添加
这里项目未实际使用,直接粘贴的文档的内容。
add_library(example_library
source1.cxx
source2.cxx
source3.cxx
source4.cxx)
set_target_properties(example_library PROPERTIES
UNITY_BUILD_MODE GROUP
)
set_source_files_properties(source1.cxx source2.cxx source3.cxx
PROPERTIES UNITY_GROUP "bucket1"
)
set_source_files_properties(source4.cxx
PROPERTIES UNITY_GROUP "bucket2"
)
补充
项目中未使用的内容
- 编译速度-使用PCH,使用后未发现明显的加速效果。
- 链接速度-使用多线程链接器,加快链接速度,目前链接很快,倾向于不做改动。
加快部署速度
简单的快速部署方式:常用的SSH工具由于权限一般不能使用,问GPT生成一个带账号密码以及能够接受文件的HTTPServer,功能只需要在接受文件并校验后,替换文件,执行服务器重启脚本。可以将脚本输出放到HTTP的回包中,上传文件和接收文件时进行压缩和解压缩,降低对带宽的需求。
需要部署服务器时可以通过curl命令直接上传文件后触发部署。