1. 为什么还要手动编译 GCC 9.3.0
1.1 系统自带编译器不够用的真实场景
大部分 Linux 发行版自带的 GCC 版本都偏保守。比如某些长期支持版本默认给的是 GCC 7.x 甚至 5.x,这些版本对 C++17 的支持是残缺的——std::filesystem还在experimental命名空间里,if constexpr可能报错,结构化绑定(structured bindings)时灵时不灵,模板推导指引(deduction guides)更是想都别想。你写了一段自认为标准的 C++17 代码,结果编译报了一屏错误,查半天发现是编译器版本的问题,这种体验相当消耗耐心。
我当初决定手动编译 GCC 9.3.0,就是因为一个项目里用到了std::filesystem做跨平台路径处理,系统自带的 GCC 7.5 死活编不过,加-lstdc++fs也只是把链接错误从一百行减到五十行。与其在代码里到处写兼容宏,不如直接把编译器升上去,一劳永逸。
GCC 9.3.0 这个版本值得选,原因有几个:它是 GCC 9.x 系列的最后一个维护版本,bug 修复比较完整;对 C++17 的核心特性支持已经全部落地,包括std::optional、std::variant、std::any、std::string_view、并行算法等;同时它还没有 GCC 10 之后那些比较激进的变化,稳定性好,作为生产环境的编译器很合适。
1.2 手动编译和包管理器安装的取舍
有人会问,为什么不直接用包管理器装个新版本?比如在某些发行版上加个第三方源就能装 GCC 9。这个方案不是不行,但有几个隐患:第三方源的编译器可能被裁剪过,缺少某些语言前端或者库支持;不同发行版的打包方式不一样,换一台机器就得重新折腾;最关键的是,你无法控制编译选项,比如--enable-languages到底开了哪些语言,--with-arch优化到了什么程度,这些在排查问题时很重要。
手动编译的好处是全程可控。你知道每一个configure参数是什么意思,知道装到了哪个路径,知道依赖库是哪个版本。出了问题也能定位到具体环节,而不是对着包管理器的黑盒干瞪眼。代价就是编译时间长——在一台四核八线程的机器上,完整编译 GCC 9.3.0 大概需要四十分钟到一个小时,取决于你开了哪些语言前端。
如果你只是想在本地跑几个 C++17 的小 demo,不想折腾编译,那用包管理器或者容器镜像里的新版 GCC 完全够用。但如果你需要在特定环境下长期维护一套工具链,或者要对编译器的行为做精细控制,手动编译是绕不过去的路。
2. 编译前的依赖准备与检查
2.1 必备依赖库清单
GCC 的编译依赖不算特别复杂,但缺一个就会在configure或者make阶段报错。下面这张表是我整理的核心依赖,按重要性排序:
| 依赖库 | 最低版本 | 作用 | 缺失后果 |
|---|---|---|---|
| GMP | 4.3.2+ | 任意精度算术运算 | configure 直接失败 |
| MPFR | 2.4.2+ | 多精度浮点运算 | configure 直接失败 |
| MPC | 0.8.0+ | 复数运算 | configure 直接失败 |
| ISL | 0.15+ | 循环优化和多项式分析 | 可选,但建议装上 |
| zlib | 1.2.3+ | 压缩支持 | 链接阶段报错 |
| binutils | 2.30+ | 汇编器和链接器 | 编译过程报错 |
| make | 3.80+ | 构建工具 | 无法执行构建 |
| gcc (旧版) | 4.8+ | 引导编译器 | 无法开始编译 |
GMP、MPFR、MPC 这三个是硬性依赖,缺一不可。它们的作用是给 GCC 提供高精度的数学运算能力,编译器内部做常量折叠、循环优化的时候会用到。ISL 是可选依赖,但如果你想让 GCC 的 Graphite 循环优化框架正常工作,最好装上。
2.2 依赖库的获取与编译
这些依赖库有两种获取方式:用系统包管理器装开发版,或者从源码编译。我建议优先用包管理器,省时间。以常见的包管理器为例:
# Debian/Ubuntu 系 sudo apt-get install libgmp-dev libmpfr-dev libmpc-dev libisl-dev zlib1g-dev # RHEL/CentOS 系 sudo yum install gmp-devel mpfr-devel libmpc-devel isl-devel zlib-devel如果包管理器里的版本太老,或者你需要在没有 root 权限的环境下安装,那就得从源码编译。源码编译的顺序很重要:先 GMP,再 MPFR,最后 MPC,因为 MPFR 依赖 GMP,MPC 依赖 GMP 和 MPFR。编译时统一指定安装前缀,比如$HOME/local,方便后续管理。
# 以 GMP 为例 tar -xvf gmp-6.2.0.tar.xz cd gmp-6.2.0 ./configure --prefix=$HOME/local --enable-cxx make -j$(nproc) make installMPFR 编译时需要指定 GMP 的路径:
./configure --prefix=$HOME/local --with-gmp=$HOME/localMPC 同理,需要同时指定 GMP 和 MPFR:
./configure --prefix=$HOME/local --with-gmp=$HOME/local --with-mpfr=$HOME/local这里有个容易踩的坑:编译完依赖库之后,一定要把
$HOME/local/lib加到LD_LIBRARY_PATH里,否则后续编译 GCC 时链接器找不到这些库。可以在~/.bashrc里加一行export LD_LIBRARY_PATH=$HOME/local/lib:$LD_LIBRARY_PATH,然后source一下。
2.3 磁盘空间与内存的硬性要求
编译 GCC 是个资源密集型任务,提前检查一下磁盘和内存很有必要。源码包解压后大概 800MB 左右,但构建目录会膨胀到 3GB 到 5GB,安装目录又要 2GB 到 3GB。所以至少预留 10GB 的磁盘空间比较稳妥。
内存方面,make的时候如果并行度开太高,每个编译进程都会吃内存。我实测下来,-j4的情况下峰值内存占用大概在 2GB 到 3GB。如果机器只有 2GB 内存,建议用-j1或者-j2,虽然慢一点,但不会因为 OOM 被系统杀掉进程。被 OOM Killer 干掉编译进程的体验很差,因为没有任何错误提示,就是突然中断了。
3. GCC 9.3.0 的配置与编译实操
3.1 configure 参数逐条解读
下载好 GCC 9.3.0 的源码包之后,解压到一个目录,然后在源码目录之外新建一个构建目录。这一点很重要,GCC 官方推荐 out-of-tree 构建,避免污染源码目录,也方便出错后直接删掉构建目录重来。
tar -xvf gcc-9.3.0.tar.xz mkdir gcc-9.3.0-build cd gcc-9.3.0-build接下来是configure命令,我把自己用的参数列出来,然后逐条解释:
../gcc-9.3.0/configure \ --prefix=$HOME/gcc-9.3.0 \ --enable-languages=c,c++ \ --disable-multilib \ --with-gmp=$HOME/local \ --with-mpfr=$HOME/local \ --with-mpc=$HOME/local \ --with-isl=$HOME/local \ --enable-checking=release \ --disable-bootstrap--prefix指定安装路径,我习惯装到用户目录下,避免污染系统目录,也不需要 root 权限。--enable-languages=c,c++只编译 C 和 C++ 前端,如果你需要 Fortran 或者 Go,可以加上对应的语言名,但编译时间会显著增加。--disable-multilib关掉 32 位库的编译,除非你确实需要编译 32 位程序,否则关掉能省不少时间。
--with-gmp等参数指定依赖库的路径,如果你是用包管理器装的依赖,这些可以省略,configure会自动找到。--enable-checking=release关掉编译器的内部断言检查,生成的编译器会快一些,适合生产使用。--disable-bootstrap是争议比较大的一个选项——bootstrap 会用刚编译出来的编译器再编译一遍自己,验证编译器的正确性,但会让编译时间翻倍。如果你对编译器质量要求极高,可以不加这个参数;如果只是日常使用,关掉能省一半时间。
3.2 编译过程中的资源控制
configure跑完之后,会生成 Makefile。接下来就是make了。这里的关键是控制并行度:
make -j$(nproc) 2>&1 | tee build.log-j$(nproc)会按照 CPU 核心数开并行任务,速度最快。但前面说过,内存不够的时候要降并行度。另外,把输出重定向到build.log是个好习惯,编译过程中如果报错,可以回头翻日志,不用在终端里往上滚。
编译过程中有几个阶段特别慢:libstdc++-v3的编译、stage1的编译器构建、以及最后的libgcc和libstdc++的链接。这几个阶段 CPU 和内存占用都会飙升,机器可能会变得很卡,属于正常现象。
如果你在编译过程中看到某个文件编译了很久没动静,不要急着 Ctrl+C。GCC 的某些源文件(比如
insn-emit.c)生成和编译都很耗时,耐心等一等。我见过有人以为卡死了就中断,结果白编译了半小时。
3.3 安装与验证
编译完成后,执行安装:
make install安装过程比较快,主要是拷贝文件。装完之后,把$HOME/gcc-9.3.0/bin加到PATH的最前面,确保优先使用新编译器:
export PATH=$HOME/gcc-9.3.0/bin:$PATH然后验证版本:
gcc --version g++ --version如果输出显示gcc (GCC) 9.3.0,说明安装成功。接下来写一个测试文件,验证 C++17 特性是否可用:
#include <filesystem> #include <optional> #include <variant> #include <iostream> int main() { std::filesystem::path p = "/tmp/test"; std::optional<int> opt = 42; std::variant<int, std::string> var = "hello"; std::cout << "path: " << p << std::endl; std::cout << "opt: " << opt.value() << std::endl; std::cout << "var: " << std::get<std::string>(var) << std::endl; return 0; }编译命令:
g++ -std=c++17 -o test_cpp17 test_cpp17.cpp ./test_cpp17如果编译通过并且运行输出正常,说明 GCC 9.3.0 的 C++17 支持已经就绪。注意std::filesystem在 GCC 9 里已经不需要额外链接-lstdc++fs了,这是 GCC 8 和 9 的一个重要区别。
4. C17 与 C++17 特性在 GCC 9.3.0 上的实测表现
4.1 C17 特性的支持情况
C17(也叫 C18)本质上是对 C11 的一个小修订,没有引入大的新特性,主要是修复了一些缺陷、澄清了一些模糊表述。GCC 9.3.0 对 C17 的支持是完整的,用-std=c17或者-std=iso9899:2017就能启用。
C17 里比较值得关注的改动包括:_Generic的改进、_Static_assert的完善、以及对原子操作的进一步规范。这些改动在日常编码中感知不强,但如果你写的是需要严格符合标准的库代码,用 C17 比 C11 更稳妥。
实测下来,GCC 9.3.0 在-std=c17模式下编译大型 C 项目(比如一些开源的基础库)没有遇到任何问题。-pedantic模式下也没有额外的警告,说明标准符合度很高。
4.2 C++17 核心特性的实测清单
C++17 引入的特性比较多,我挑几个最常用的,在 GCC 9.3.0 上逐个验证:
| 特性 | 头文件 | GCC 9.3.0 支持 | 备注 |
|---|---|---|---|
| 结构化绑定 | 语言特性 | 完整支持 | auto [a, b] = pair; |
| if constexpr | 语言特性 | 完整支持 | 编译期分支 |
| std::optional | <optional> | 完整支持 | 可能为空的返回值 |
| std::variant | <variant> | 完整支持 | 类型安全的联合体 |
| std::any | <any> | 完整支持 | 任意类型容器 |
| std::string_view | <string_view> | 完整支持 | 非拥有字符串视图 |
| std::filesystem | <filesystem> | 完整支持 | 无需额外链接 |
| 并行算法 | <algorithm> | 需要 TBB | 链接-ltbb |
| 折叠表达式 | 语言特性 | 完整支持 | (args + ...) |
| 类模板参数推导 | 语言特性 | 完整支持 | std::pair p{1, 2}; |
并行算法这一项需要特别注意。GCC 9.3.0 的并行算法实现依赖 Intel TBB(Threading Building Blocks),如果你要用std::execution::par策略,需要安装 TBB 并在编译时链接-ltbb。如果没装 TBB,编译会报错说找不到tbb/tbb.h。这是很多人容易忽略的一个依赖。
4.3 从 C++14 迁移到 C++17 的注意事项
如果你手上有 C++14 的代码要迁移到 C++17,有几个地方需要留意。首先是std::auto_ptr在 C++17 里被正式移除了,如果代码里还在用,编译会直接报错,需要换成std::unique_ptr。其次是std::random_shuffle也被移除了,要换成std::shuffle。
另外,C++17 对noexcept的检查更严格了,某些在 C++14 下能编过的代码,在 C++17 下可能会因为异常规格不匹配而报错。遇到这种情况,检查一下函数声明和定义里的noexcept是否一致。
还有一个比较隐蔽的变化:C++17 引入了保证的拷贝省略(guaranteed copy elision),某些依赖拷贝构造函数的代码行为会发生变化。比如T obj = T();在 C++17 里不会调用拷贝构造函数,而是直接在目标位置构造。如果你的类依赖拷贝构造函数做副作用,迁移时要注意。
5. 常见编译问题与排查技巧
5.1 configure 阶段的典型报错
报错一:找不到 GMP/MPFR/MPC
configure: error: Building GCC requires GMP 4.2+, MPFR 2.4.0+ and MPC 0.8.0+.这个报错通常是因为依赖库没装,或者装了但configure找不到。解决办法:确认依赖库的安装路径,然后用--with-gmp、--with-mpfr、--with-mpc显式指定。如果依赖是装在非标准路径下的,还要确保LD_LIBRARY_PATH包含了对应的lib目录。
报错二:C++ 编译器缺失
configure: error: C++ compiler missing or inoperationalGCC 的构建过程需要用一个已有的 C++ 编译器来编译部分工具。如果系统里只有 C 编译器没有 C++ 编译器,就会报这个错。装一个g++就行。
报错三:ISL 版本不匹配
configure: error: ISL version 0.15 or later is requiredISL 的版本要求比较严格,太老的不行。如果包管理器里的 ISL 版本不够,就从源码编译一个。ISL 本身编译很快,几分钟就能搞定。
5.2 make 阶段的典型报错
报错一:内存不足被 killed
make: *** [Makefile:xxx: xxx] Killed没有其他错误信息,就是 Killed,基本可以确定是 OOM。解决办法是降低并行度,用-j2或者-j1重新编译。如果已经编译了一部分,不用删掉构建目录,直接重新make就行,已经编译好的目标文件不会重复编译。
报错二:链接时找不到依赖库
undefined reference to `__gmpz_init'这是链接器找不到 GMP 库的符号。检查LD_LIBRARY_PATH是否包含了 GMP 的lib目录,或者用-L参数显式指定库路径。有时候还需要用-Wl,-rpath把库路径写进可执行文件里,避免运行时找不到。
报错三:编译到一半磁盘满了
No space left on device这个没什么好说的,清理磁盘空间,或者换一个分区重新来。建议在开始编译之前就用df -h检查一下可用空间。
5.3 安装后的运行时问题
问题一:新编译器找不到标准库头文件
装完 GCC 9.3.0 之后,编译一个简单的 C++ 程序,报错说找不到<iostream>。这通常是因为PATH里新编译器的路径没放对,或者CPLUS_INCLUDE_PATH被旧编译器污染了。检查which g++的输出,确认指向的是新编译器的路径。
问题二:链接时用了旧版的 libstdc++
程序编译通过,但运行时崩溃,报错说GLIBCXX_3.4.26 not found。这是因为运行时链接器找到了系统自带的旧版libstdc++,而不是新编译器配套的那个。解决办法是在链接时加上-Wl,-rpath,$HOME/gcc-9.3.0/lib64,把新库的路径写进可执行文件。
问题三:并行算法链接失败
用了std::execution::par但链接报错,提示找不到 TBB 的符号。确认 TBB 是否安装,编译时是否加了-ltbb。如果 TBB 装在非标准路径,还要加-L和-Wl,-rpath。
我踩过最坑的一个问题是:编译出来的程序在本地跑得好好的,放到另一台机器上就报
GLIBCXX版本不匹配。后来发现是那台机器上的libstdc++太老。解决办法要么是静态链接libstdc++(加-static-libstdc++),要么是把新版的libstdc++一起打包过去。静态链接最省事,但可执行文件会大一些。
6. 工具链的长期维护建议
6.1 多版本 GCC 的共存管理
手动编译的 GCC 装在用户目录下,和系统自带的编译器天然隔离,这本身就是一种共存方案。但如果你需要频繁切换不同版本的 GCC,手动改PATH就比较麻烦了。可以用update-alternatives来管理,或者写一个简单的 shell 函数:
use_gcc() { export PATH=$HOME/gcc-$1/bin:$PATH export LD_LIBRARY_PATH=$HOME/gcc-$1/lib64:$LD_LIBRARY_PATH }这样use_gcc 9.3.0就能切到 9.3.0,use_gcc 11.2.0就能切到 11.2.0。切换之后用gcc --version确认一下。
6.2 编译产物的清理与空间回收
GCC 的构建目录很大,装完之后如果不需要保留构建中间产物,可以直接删掉构建目录,能回收好几个 GB 的空间。安装目录本身大概 2GB 到 3GB,如果磁盘紧张,可以删掉一些不常用的语言前端和文档。
另外,make install之后,源码目录和构建目录都可以删掉,只保留安装目录就行。如果以后需要重新编译,重新解压源码包即可。
6.3 什么情况下值得升级到更新的版本
GCC 9.3.0 对 C++17 的支持已经足够完整,但如果你要用 C++20 的特性,比如 concepts、ranges、coroutines,那就需要 GCC 10 或更高版本。GCC 10 对 C++20 的支持是实验性的,GCC 11 和 12 才比较完善。
我的建议是:如果项目锁定在 C++17,GCC 9.3.0 可以用很长时间,没必要追新。如果项目计划迁移到 C++20,那直接上 GCC 11 或 12,跳过 10。每次大版本升级都会带来一些不兼容的变化,升级之前先在测试环境里跑一遍完整的编译和测试,确认没有问题再推到生产环境。
编译 GCC 这件事,第一次做会觉得步骤繁琐,但走通一遍之后,后面再编译其他版本就是重复劳动了。把依赖装好、参数配对、日志留好,基本不会出大问题。真正耗时的就是编译本身,泡杯茶等着就行。