优化跳转与分支
在现代处理器流水线中,指令和数据是分阶段获取和解码的。当遇到分支指令时,处理器会尝试预测将执行哪个分支,并从该分支获取和解码指令。然而,如果分支预测错误,处理器需要花费 10 个或更多时钟周期 才能检测到错误。随后,它必须花费更多时钟周期从正确的分支重新获取指令并执行。因此,分支预测错误会浪费大量时钟周期。
C++ 中常见的跳转与分支形式
1. if-else
分支 if-else
是最常见的分支形式。当条件链较长时,应尽量避免,因为随着条件增多,预测正确的难度会显著增加。优化方法是:
• 减少条件数量。
• 结构化条件,使其更易预测。
2. for
和 while
循环
循环本质上是分支的一种,通常在循环计数较小时预测效果较好。然而,嵌套循环或包含难以预测退出条件的循环会使分支预测变得复杂。
3. switch
语句 switch
是一种具有多个跳转目标的分支形式,因此难以预测。如果 case
标签值分布较广,编译器可能将其实现为一系列 if-else
分支树,效率较低。
优化技巧:
• 将 case
标签值按升序排列且递增 1(如 1, 2, 3
),这样编译器更可能将其实现为 跳转表(jump table),效率显著提高。
4. 替换分支为查表法
在源代码中,用查表法替代分支是一种有效的优化方法。例如,通过预先计算不同输入对应的输出值并存储在表中,直接查表获取结果。
• 也可以创建函数指针表,根据跳转条件索引调用函数,但函数指针的效率通常并不比分支本身高很多。
5. 循环展开(Loop Unrolling)
如果循环中包含难以预测的分支,可能导致大量分支预测错误。通过 循环展开,可以减少分支检查的次数。
• 原理:将循环体复制多次,避免重复的分支判断。
• 实现方式:编译器通常会尝试自动展开循环,但手动展开效果更好。
示例:
以下是一个简单的循环(loop_unroll.cpp
,GitHub 第三章):
int a[5]; a[0] = 0; for (int i = 1; i < 5; ++i) a[i] = a[i - 1] + 1;
编译器可能将其展开为:
int a[5]; a[0] = 0; a[1] = a[0] + 1; a[2] = a[1] + 1; a[3] = a[2] + 1; a[4] = a[3] + 1;
注意:对于更复杂的循环,编译器可能会结合其他优化进一步简化代码。
6. 编译时分支(if constexpr
)
使用 if constexpr (condition-expression) {}
可以在编译时完成分支判断,将分支开销转移到编译阶段。
• 要求:condition-expression
必须是编译时可计算的表达式。
• 原理:这属于 编译时多态 或 模板元编程 的一部分,我们将在后续章节详细讨论。
7. 分支预测提示
开发者可以通过源代码提供分支预测提示,帮助处理器优化分支预测。
• GNU C++ 中使用 __builtin_expect
:
#define LIKELY_CONDITION(x) __builtin_expect(!!(x), 1) #define UNLIKELY_CONDITION(x) __builtin_expect(!!(x), 0)
• LIKELY_CONDITION(x)
:提示 x
很可能为真。
• UNLIKELY_CONDITION(x)
:提示 x
很可能为假。
• C++20 标准化:引入了 [[likely]]
和 [[unlikely]]
属性:
if (LIKELY_CONDITION(condition)) { // 可能执行的代码 } else { // 不太可能执行的代码 }
注意:现代处理器通常能通过多次迭代学习分支行为,因此分支预测提示的整体影响有限,但在某些特定场景下仍有优化价值。
系统当前共有 442 篇文章