为什么C++编译那么慢?—— 一个“抄书”的笨游戏
SCons 是 Python 写的构建工具,跨平台(Windows/Linux/macOS 通吃),它的核心优势是依赖分析比 Make 更智能——这对我们做“空间换时间”特别有利。
SCons 的基础认知(高中生友好版)
- SCons 的配置文件叫
SConstruct(相当于 Make 的Makefile,但它是 Python 脚本) - SCons 默认就是增量编译!它自动计算源文件和目标文件的 MD5 签名,而不是只看时间戳(比 Make 更可靠)
- 编译命令:终端里直接敲
scons即可
好,接下来我们分四招,每一招都给出完整的 SConstruct 代码,你复制粘贴就能跑。
第一招:增量编译(SCons 自带,零配置)
原理
SCons 会为每个目标文件(.o)生成一个 .sconsign 数据库,里面存着源文件的 MD5 哈希值。只有 MD5 变了,才重新编译。
实际操作(什么都不用做!)
# 第一次编译
scons
# 修改了一个 .cpp 文件后再次编译
scons # SCons 自动只重编修改的那个文件空间代价:根目录下会生成 .sconsign.dblite 文件(几十 MB),记录所有依赖关系。
强制全量重编(不推荐,但你知道就行)
scons -c # 清理所有构建产物
scons # 全量重编平时别用 -c,除非你要发布源码包。
第二招:预编译头(PCH)—— SCons 原生支持
术语回顾
PCH 就是把 <iostream> 这种万年不变的大头文件提前编译成 .gch(GCC)或 .pch(Clang),后续编译直接加载二进制镜像。
实际操作(SConstruct 写法)
目录结构:
project/
├── SConstruct
├── pch.h # 放所有稳定的大头文件
├── pch.cpp # 空文件,专门用来生成 PCH
├── main.cpp
└── utils.cppSConstruct 完整代码:
env = Environment(
CXX='g++', # 或 'clang++'
CCFLAGS=['-Wall'],
CPPPATH=['.'] # 头文件搜索路径
)
# 1. 单独构建预编译头
pch_env = env.Clone()
pch_obj = pch_env.PCH(target='pch', source='pch.cpp')
# PCH() 是 SCons 内置 Builder,自动生成 .gch 文件
# 2. 主程序编译,使用 PCH
main_env = env.Clone()
# 让所有源文件依赖这个 PCH 文件
main_env.Append(CCFLAGS=['-include', 'pch.h']) # GCC/Clang 自动包含 PCH
# 3. 构建目标文件
main_obj = main_env.Object('main.cpp')
utils_obj = main_env.Object('utils.cpp')
# 4. 链接成可执行文件
main_env.Program('myapp', [main_obj, utils_obj])
# 5. 设置依赖:所有 .o 文件依赖 pch 的构建产物
Depends(main_obj, pch_obj)
Depends(utils_obj, pch_obj)编译命令:
scons第一次编译会先生成 pch.gch(几百 MB),后续编译直接复用。
空间代价:pch.gch 文件可能 200~500 MB,但换来的好处是 main.cpp 和 utils.cpp 编译时不再解析 STL 头文件。
第三招:拆分为静态库(Static Library)—— 模块级增量编译
术语回顾
把稳定的模块(如网络库、数学库)单独编译成 .a(Linux)或 .lib(Windows)文件,主程序只链接这些库。
实际操作(SConstruct 写法)
目录结构:
project/
├── SConstruct
├── network/ # 稳定模块 A
│ ├── socket.cpp
│ └── tcp.cpp
├── database/ # 稳定模块 B
│ ├── mysql.cpp
│ └── redis.cpp
└── app/ # 频繁修改的业务模块
├── main.cpp
└── handler.cppSConstruct 完整代码:
env = Environment(
CXX='g++',
CCFLAGS=['-O2', '-Wall'],
CPPPATH=['#/network', '#/database'] # # 表示 SConstruct 所在目录
)
# 1. 编译 network 模块为静态库
network_src = Glob('network/*.cpp')
network_lib = env.StaticLibrary('network', network_src) # 生成 libnetwork.a
# 2. 编译 database 模块为静态库
database_src = Glob('database/*.cpp')
database_lib = env.StaticLibrary('database', database_src) # 生成 libdatabase.a
# 3. 编译 app 模块的源文件
app_src = Glob('app/*.cpp')
app_objs = env.Object(app_src)
# 4. 链接所有库和对象文件成可执行文件
env.Program('myapp', app_objs + [network_lib, database_lib])效果
- 你只修改了
app/handler.cpp - 运行
scons时,network 和 database 模块完全不重编 - 控制台输出只有:
g++ -o app/handler.o -c app/handler.cpp
g++ -o myapp app/main.o app/handler.o libnetwork.a libdatabase.a空间代价:同时保留 libnetwork.a(50MB)和 libdatabase.a(80MB),但修改 app 时编译时间从 5 分钟变成 10 秒。
第四招:SCons 的“变体目录”(VariantDir)—— 把不同编译配置分开放
术语解释
如果你的项目要同时编译 Debug 和 Release 版本,或者同时编译 Linux 和 Windows,你不能让目标文件混在一起(否则会互相覆盖或冲突)。
VariantDir 是 SCons 的杀手级功能:它把不同配置的 .o 和 .a 文件放在不同的目录里,但源码只有一份。
实际操作
env_debug = Environment(
CXX='g++',
CCFLAGS=['-g', '-O0'] # Debug 配置
)
env_release = Environment(
CXX='g++',
CCFLAGS=['-O3'] # Release 配置
)
# 创建两个构建目录
VariantDir('build/debug', 'src', duplicate=0) # duplicate=0 表示不复制源码,直接引用
VariantDir('build/release', 'src', duplicate=0)
# 分别编译
debug_objs = env_debug.Object(Glob('build/debug/*.cpp'))
release_objs = env_release.Object(Glob('build/release/*.cpp'))
env_debug.Program('build/debug/myapp', debug_objs)
env_release.Program('build/release/myapp', release_objs)好处:
- 修改源码后,Debug 和 Release 各自独立增量编译
- 你可以同时开着两个终端,一个编译 Debug,一个编译 Release,互不干扰
第五招(高阶):SCons + C++20 Module(前沿玩法)
C++20 Module 目前 GCC/Clang 支持还不完善,但 SCons 可以通过自定义 Builder 来支持。下面是个实验性示例(GCC 13+ 或 Clang 16+):
目录结构
project/
├── SConstruct
├── math.ixx # 模块接口文件
└── main.cppSConstruct 代码
env = Environment(
CXX='g++',
CXXFLAGS=['-std=c++20', '-fmodules-ts'] # GCC 开启模块支持
)
# 自定义构建模块接口
env.Builder(
action='$CXX $CXXFLAGS -c $SOURCE -o $TARGET',
suffix='.o',
src_suffix='.ixx'
)
# 编译模块
math_obj = env.Object('math.ixx')
# 编译主程序(导入模块)
main_obj = env.Object('main.cpp')
# 链接
env.Program('myapp', [math_obj, main_obj])注意:C++20 模块在 SCons 中还没有原生 Module() Builder,上面的代码需要根据你编译器的实际行为调整。生产环境建议只用在第四招(静态库拆分),模块可以先了解。
针对 SCons 的特别优化技巧(加分项)
1. 使用 CacheDir 共享编译缓存(团队协作神器)
如果你的团队多人开发,可以在 SConstruct 中设置:
env = Environment()
env.CacheDir('/shared/cache') # 所有编译产物存到共享目录其他人编译时,如果 MD5 相同,直接复制缓存中的 .o 文件,无需重新编译——这是用网络存储空间换全团队的时间。
2. 开启并行编译
scons -j 4 # 4 核并行编译SCons 会自动检测依赖关系,不会因为并行而出错。
3. 使用 ParseConfig 自动拉取第三方库
env.ParseConfig('pkg-config --cflags --libs opencv4')这相当于自动把 OpenCV 的编译参数加进来,避免手动写长长的 -I 和 -L。
总结一页纸(SCons 专属版)
| 技术 | SCons 写法 | 一句话效果 |
|---|---|---|
| 增量编译 | 默认就是,不用写任何代码 | 只编译改过的 .cpp |
| 预编译头 | PCH('pch', 'pch.cpp') + CCFLAGS=['-include','pch.h'] | 大头文件只解析一次 |
| 静态库拆分 | StaticLibrary('模块名', 源码列表) | 稳定模块编译成 .a,改业务模块时零重编 |
| 不同配置隔离 | VariantDir('build/debug', 'src') | Debug 和 Release 的 .o 分开存放 |
| 缓存共享 | CacheDir('/shared/path') | 团队共用编译缓存,新人不用全量编译 |
给你的课后小实验(SCons 动手做)
在 Linux 终端里跑一遍:
# 1. 安装 SCons(如果没有)
pip install scons
# 2. 创建测试项目
mkdir test_project && cd test_project
# 3. 写一个 SConstruct(复制上面第三招的静态库拆分代码)
# 4. 创建对应的 .cpp 文件(network/socket.cpp, database/mysql.cpp, app/main.cpp)
# 5. 第一次编译
scons
# 6. 修改 app/main.cpp,再次编译
scons # 观察输出,network 和 database 是否被重编你会看到 SCons 的控制台输出里,只有 app/main.o 被重新编译,network 和 database 直接用了已有的 .a 文件。
这样,不管你在 Windows 还是 Linux,用 SCons 还是 Make,都有了一整套“空间换时间”的打法了。你现在的环境具体是 SCons + GCC 还是 SCons + Clang?如果有报错,把 scons 输出的错误贴出来,我帮你调参数。🚀