5-4 假设规则与编译时优化
优化介绍
在编程中,优化optimization是指通过修改软件使其运行更高效的过程(例如运行更快或消耗更少资源)。优化对应用程序的整体性能水平具有重大影响。
某些类型的优化通常需要手动完成。程序员可借助性能分析器profiler观察程序各部分的运行时长,识别影响整体性能的瓶颈,进而寻找解决途径。由于手动优化效率较低,程序员通常专注于实施影响显著的高层级改进(如选用更高性能的算法、优化数据存储与访问、降低资源占用、任务并行化等)。
其他类型的优化可自动完成。用于优化程序的程序称为优化器。优化器optimizer通常在低级别运作,通过重写、重新排序或消除语句/表达式来提升效率。例如将i = i * 2;优化为i *= 2;、i += i;或i <<= 1;。对于整数值,这些形式结果相同,但在特定架构下可能存在执行效率差异。程序员通常无法判断最佳选择(且答案随架构变化),但特定系统的优化器能做到。单项低级优化带来的性能提升或许微小,但累积效应可显著提升整体运行效率。
现代C++编译器均具备优化能力,能在编译过程中自动优化程序。与预处理器类似,这些优化不会修改源代码文件——而是作为编译流程的透明环节自动实施。
关键洞见
优化编译器使程序员能够专注于编写可读性强且易于维护的代码,同时不会牺牲性能。
由于优化涉及某些权衡(我们将在本课末尾讨论),编译器通常支持多种优化级别,这些级别决定了是否进行优化、优化的强度以及优先级(例如速度与体积的权衡)。
大多数编译器默认不进行优化,因此若使用命令行编译器,需手动启用优化功能。若使用集成开发环境(IDE),通常会自动配置:发布构建启用优化,调试构建则禁用优化。
针对 gcc 和 Clang 用户
请参阅 0.9 -- 配置您的编译器:构建配置,了解如何启用优化功能。
假设规则
在C++中,编译器拥有很大的自由度来优化程序。假设规则as-if rule 指出,只要修改不影响程序的“可观察行为”,编译器可以随意修改程序以生成更优化的代码。
对于高级读者
“如同”规则有一个值得注意的例外:即使某些构造函数具有可观察行为,仍可省略对复制(或移动)构造函数的不必要调用。我们将在第14.15节——类初始化和复制省略中探讨此主题。
现代编译器采用多种不同技术来有效优化程序。具体可应用的技术取决于程序本身以及编译器和优化器的性能。
相关内容
维基百科列出了编译器使用的具体技术清单。
一个优化机会
请考虑以下简短程序:
#include <iostream>
int main()
{
int x { 3 + 4 };
std::cout << x << '\n';
return 0;
}
输出结果很简单:

然而,其中隐藏着一个有趣的优化可能性。
如果该程序完全按原样编译(不进行任何优化),编译器生成的可执行文件将在运行时(程序执行时)计算3+4的结果。若程序被执行一百万次,3+4将被计算一百万次,最终值7也将被生成一百万次。
由于3+4的结果永远不变(始终为7),每次运行程序都重新计算这个结果是浪费资源的行为。
编译时求值
现代C++编译器能够在编译时(而非运行时)对某些表达式进行完全或部分求值。当编译器在编译时对表达式进行完全或部分求值时,称为编译时求值compile-time evaluation。
核心要义
编译时求值使编译器能够在编译阶段完成原本需在运行时执行的工作。由于此类表达式无需在运行时进行求值,生成的可执行文件运行速度更快且体积更小(代价是编译时间略有延长)。
为便于说明,本节将介绍若干利用编译时评估的简单优化技术,后续课程将延续对编译时评估的探讨。
常量折叠
编译时评估的原始形式之一称为“常量折叠”。常量折叠Constant folding是一种优化技术,编译器通过将包含字面量操作数的表达式替换为该表达式的结果来实现。利用常量折叠,编译器会识别出表达式 3 + 4 具有常量操作数,并将其替换为结果 7。
其效果等同于以下操作:
#include <iostream>
int main()
{
int x { 7 };
std::cout << x << '\n';
return 0;
}
该程序与先前版本输出相同(7),但生成的可执行文件不再需要在运行时消耗CPU周期计算3+4!
常量折叠也可应用于子表达式,即使整个表达式必须在运行时执行。
#include <iostream>
int main()
{
std::cout << 3 + 4 << '\n';
return 0;
}
在上例中,3 + 4 是完整表达式 std::cout << 3 + 4 << ‘\n’; 的子表达式。编译器可将其优化为 std::cout << 7 << ‘\n’;。
常量传播
以下程序包含另一个优化机会:
#include <iostream>
int main()
{
int x { 7 };
std::cout << x << '\n';
return 0;
}
当x被初始化时,值7将被存储在为x分配的内存中。随后在下一行代码中,程序需再次访问内存获取值7以便输出。这需要两次内存访问操作(一次存储值,一次读取值)。
常量传播Constant propagation是一种优化技术,编译器会将已知具有常量值的变量替换为其值。通过常量传播,编译器会意识到x始终具有常量值7,并将变量x的所有使用替换为值7。
结果将等同于以下代码:
#include <iostream>
int main()
{
int x { 7 };
std::cout << 7 << '\n';
return 0;
}
这消除了程序需要访问内存来获取x值的必要性。
常量传播可能产生可通过常量折叠进一步优化的结果:
#include <iostream>
int main()
{
int x { 7 };
int y { 3 };
std::cout << x + y << '\n';
return 0;
}
在此示例中,常量传播会将 x + y 转换为 7 + 3,随后可通过常量折叠将其折叠为值 10。
死代码消除
死代码消除是一种优化技术,编译器通过该技术移除可能被执行但对程序行为无影响的代码。
回到之前的示例:
#include <iostream>
int main()
{
int x { 7 };
std::cout << 7 << '\n';
return 0;
}
在此程序中,变量 x 被定义并初始化,但从未在任何地方使用,因此它对程序行为没有任何影响。死代码消除将移除 x 的定义。
结果将等同于以下代码:
#include <iostream>
int main()
{
std::cout << 7 << '\n';
return 0;
}
当变量因不再需要而从程序中移除时,我们称该变量已被优化掉optimized out(或优化消除optimized away)。
相较于原始版本,这个优化后的版本不再需要运行时计算表达式 3 + 4,也不再需要两次内存访问操作(一次用于初始化变量 x,一次用于读取 x 的值)。这意味着程序将变得更小且运行更快。
常量变量更易于优化
在某些情况下,我们可以通过简单操作帮助编译器更有效地进行优化。
常量传播对编译器而言可能颇具挑战性。在常量传播章节中,我们给出了如下示例:
#include <iostream>
int main()
{
int x { 7 };
std::cout << x << '\n';
return 0;
}
由于 x 被定义为非常量变量,要应用此优化,编译器必须意识到 x 的值实际上并未改变(尽管理论上可能改变)。编译器能否实现这一点,取决于程序的复杂程度以及编译器优化例程的先进程度。
我们可以通过尽可能使用常量变量来帮助编译器更有效地优化。例如:
#include <iostream>
int main()
{
const int x { 7 }; // x is now const
std::cout << x << '\n';
return 0;
}
由于 x 现在是 const 类型,编译器可以确保 x 在初始化后不会被修改。这使得编译器更可能应用常量传播,进而将该变量完全优化掉。
关键要点
使用 const 变量有助于编译器更有效地进行优化。
优化会增加程序调试难度
既然优化能提升程序运行速度,为何不默认开启?
当编译器对程序进行优化时,变量、表达式、语句和函数调用可能会被重新排列、修改、替换甚至完全移除。这些改动会显著增加程序调试的难度。
运行时,编译后的代码与原始源代码关联性大幅降低,导致调试困难。例如:若尝试监视已被优化的变量,调试器将无法定位该变量;若尝试步入已被优化的函数,调试器会直接跳过该函数。因此当调试过程中出现异常行为时,这通常是根本原因。
编译时我们几乎无法洞察编译器的运作机制,缺乏有效工具辅助理解其行为。若变量或表达式被错误值替换,我们该如何排查问题?这始终是亟待解决的难题。
为尽量减少此类问题,调试构建通常会禁用优化功能,使编译后的代码更贴近源代码。
作者注
编译时调试仍是发展不足的领域。截至C++23版本,已有若干论文(如本文)正被考虑纳入未来语言标准,若获批准,将为语言增添相关辅助功能。
命名法:编译时常量与运行时常量
C++中的常量有时被分为两类非正式分类。
编译时常量compile-time constant是指其值在编译时已知的常量。例如:
- 字面量。
- 初始化器为编译时常量的常量对象。
运行时常量runtime constant是指其值在运行时上下文中确定的常量。例如:
- 常量函数参数。
- 初始化器为非常量或运行时常量的常量对象。
例如:
#include <iostream>
int five()
{
return 5;
}
int pass(const int x) // x is a runtime constant
{
return x;
}
int main()
{
// The following are non-constants:
[[maybe_unused]] int a { 5 };
// The following are compile-time constants:
[[maybe_unused]] const int b { 5 };
[[maybe_unused]] const double c { 1.2 };
[[maybe_unused]] const int d { b }; // b is a compile-time constant
// The following are runtime constants:
[[maybe_unused]] const int e { a }; // a is non-const
[[maybe_unused]] const int f { e }; // e is a runtime constant
[[maybe_unused]] const int g { five() }; // return value isn't known until runtime
[[maybe_unused]] const int h { pass(5) }; // return value isn't known until runtime
return 0;
}
尽管你在实际编程中会遇到这些术语,但在C++中这些定义其实并不实用:
- 某些运行时常量(甚至非常量)可在编译时进行评估以实现优化(遵循“如同”规则)。
- 某些编译时常量(例如const double d { 1.2 };)无法用于编译时特性(按语言标准定义)。我们将在第5.5节“常量表达式”中深入探讨此问题。
因此建议避免使用这些术语。下一节将介绍应采用的替代命名规范。
作者注
我们正逐步将这些术语从后续文章中移除。

浙公网安备 33010602011771号