这篇博文的实验数据取自个人环境,不同编译器版本和标准库实现下结果可能不同。我在 GCC 12.2 上做了验证,测试代码也都贴了出来,你可以直接在自己的环境里跑一遍看趋势是否一致。
C++ 的 constexpr 编译期计算,性能到底比运行期快多少?这个问题我最近在项目里认真做了一次实测对比,结论有些反直觉:用对了场景快得离谱,用错了场景编译时间暴涨,运行时却毫无收益。
起因是某嵌入式项目里一张 256 项的状态转移表,原本在构造函数里用循环初始化。有人建议改成 constexpr 在编译期生成,理由是"省掉启动初始化时间"。我当时没有直接拍板,而是把运行期循环、constexpr 函数、模板元编程三种实现方式放在一起做了个基准测试,顺便把编译时间、生成代码大小也记了下来。
这篇文章就是那次对比的完整记录。适合两类人看:一类是在面试里被问到 constexpr 和普通函数区别、想知道编译期计算到底"快在哪"的开发者;另一类是正在纠结要不要把查找表或算法塞进 constexpr、担心编译时间和可维护性的工程派。两种视角我都会覆盖到。
1. constexpr 的进化史与编译期求值的真正含义
在聊性能数据之前,先把 constexpr 的能力边界搞清楚。很多性能对比结论失真,就是因为对"什么时候真的会编译期求值"理解有偏差。
1.1 从 C++11 到 C++23:constexpr 能力逐步放开
constexpr 不是一开始就那么好用的,它的演进史直接决定了你该用哪种风格写:
| 版本 | 关键能力 | 实际影响 |
|---|---|---|
| C++11 | 函数体只能包含一个 return 语句 | 只能写表达式嵌套,难看且难调试 |
| C++14 | 支持循环、局部变量、多语句 | 终于能像普通函数一样写算法 |
| C++17 | if constexpr、constexpr lambda、std::array 的 constexpr 方法 |
编译期分支、更灵活的泛型代码 |
| C++20 | consteval、constinit、constexpr 支持 new/delete、虚函数、std::vector 基础操作 |
可以在编译期做动态内存分配(受限) |
| C++23 | 标准库更多容器和算法加入 constexpr 支持 |
编译期计算能力进一步向普通代码靠拢 |
C++11 时代,想用 constexpr 算一个斐波那契数列,只能写成 return N <= 1 ? N : fib(N-1) + fib(N-2); 这种三元表达式嵌套。C++14 之后,直接在里面写循环和局部变量,和写普通函数没有区别。这个变化的意义非常大:constexpr 不再是"奇技淫巧",而是普通人也能驾驭的编译期编程工具。
1.2 写 constexpr 不等于一定会编译期求值
这是最容易踩的认知坑。
constexpr 函数只有在常量表达式语境中调用时,编译器才强制在编译期求值。比如:
- 初始化一个
constexpr变量 - 作为数组长度或模板参数
- 出现在
static_assert中 - 作为
case标签(编译期整数常量)
而在普通函数里用运行时变量调用一个 constexpr 函数,它就是普通函数,完全不保证编译期求值。看个例子:
cpp复制constexpr int add(int a, int b) {
return a + b;
}
int main() {
int x, y;
std::cin >> x >> y;
int z = add(x, y); // 运行期调用,和普通函数没有区别
constexpr int w = add(1, 2); // 编译期求值,w 等于常量 3
}
C++20 的 consteval(立即函数)才真正强制"必须编译期求值"。所以你在做性能测试时,如果只是把函数声明成 constexpr 就在普通上下文中调用,它可能根本没有编译期求值,实测结果自然就是"和运行期一样快",网上很多"constexpr 性能没有提升"的结论就是这么来的。
1.3 constexpr 性能提升的本质:成本转移而非凭空加速
想通这一点,你就能自己判断任何场景该不该用 constexpr。
运行期计算消耗的是 CPU 周期、寄存器、可能还有栈空间。编译期计算消耗的是编译器的时间、构建机器的内存。两者的关系可以类比成"提前做好预制菜"和"每餐现做":
- 提前做好(编译期计算):备菜阶段花更多时间,但客人来了之后秒上菜。
- 每餐现做(运行期计算):备菜快,但每次来人后都要等厨师炒菜。
如果一道菜只吃一次,预制菜的优势很小甚至为负。如果这道菜一天要做一万次,预制菜的优势就非常可观。这就是判断 constexpr 适用性的核心逻辑:计算次数少、使用次数多的场景最划算;只算一次用一次的场景,收益基本可以忽略,反而要承担编译时间成本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 同一个斐波那契函数,三种写法的实测差距
为了把道理讲透,我做了个非常干净的对比测试:计算斐波那契数列第 40 项,分别在主循环中调用 1000 万次。三种写法分别是运行期循环、constexpr 常量和模板元编程。
2.1 测试代码与防优化设计
先看代码。这段测试最关键的是防优化设计,很多人跑性能测试不留意这个,数据就废了。
cpp复制#include <chrono>
#include <cstdint>
#include <cstdio>
// 运行期循环版本
long long fib_runtime(int n) {
if (n <= 1) return n;
long long a =
