为什么C++编译那么慢?—— 一个“抄书”的笨游戏

📄 文章 🌐 公开
📋 列表 ✏️ 编辑 🎨 画布版 📋 复制MD ☆ 收藏

为什么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.cpp

SConstruct 完整代码

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.cpp

SConstruct 完整代码

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)—— 把不同编译配置分开放

术语解释

如果你的项目要同时编译 DebugRelease 版本,或者同时编译 LinuxWindows,你不能让目标文件混在一起(否则会互相覆盖或冲突)。

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.cpp

SConstruct 代码

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 输出的错误贴出来,我帮你调参数。🚀

💬 留言 ⋮⋮

加载中…
💡 不登录也可留言(IP 限制:每文/每天各 1/10 条)

加载中…

纸张白
护眼绿
羊皮卷
夜间黑
100%