优化 C++/代码优化/流水线
条件跳转机器语言指令(也称为 分支)可能由许多 C++ 语言特性生成,其中包括 if-else
、for
、while
、do-while
和 switch-case
语句,以及布尔和条件表达式运算符。
现代处理器仅在能够预测分支的情况下才能有效地处理分支。如果预测错误,则 流水线 在后续指令上已完成的步骤将无用,处理器必须从分支目标指令重新开始。
分支预测基于对同一指令的先前迭代。如果分支遵循规律模式,则预测会成功。
最佳情况是分支指令始终具有相同的效果;在这种情况下,预测几乎总是正确的。最糟糕的情况是分支指令的结果模式与分支预测行为相反。由于预测器通常假设结果会重复出现,因此这将是一个结果始终与先前结果相反的分支。在这种情况下,预测永远不正确,流水线始终被中断。并非所有处理器都使用这种预测行为,一些简单的处理器始终假设不会发生跳转。分支指令具有随机结果的情况会导致预测平均情况下只有一半时间是正确的。但是,随机分布意味着实际结果将从始终正确到始终错误,呈高斯形状分布。
在瓶颈中,应避免难以预测的分支。如果分支预测非常糟糕,即使用相当慢的指令序列替换它也可能导致速度提升。
在本节中,将介绍一些用等效指令替换分支的技术。
如果您需要检查一个整数 i
是否在两个整数 min_i
和 max_i
(包括)之间,并且您确定 min_i <= max_i
,请使用以下表达式
unsigned(i – min_i) <= unsigned(max_i – min_i)
在给定的条件下,上述公式等效于以下更直观的公式
min_i <= i && i <= max_i
前一个公式执行两次差值和一次比较,而后一个公式执行零次差值和两次比较。对于流水线处理器,比较比差值慢,因为它们意味着分支。 [存疑 ]
此外,如果 min_i
是一个值为零的常量表达式,则两个差值会消失。
特别是,要检查一个整数 i
是否是 size
个元素数组的有效索引,即执行数组边界检查,请使用以下表达式
unsigned(i) < unsigned(size)
显然,如果表达式已经是 unsigned
类型,则不需要转换。
不要使用条件表达式,在其中两种情况都是常量,而是使用一个有两位置的查找表。
如果您有以下语句,其中 c
和 d
代表常量表达式,b
代表布尔表达式
a = b ? c : d;
这等效于以下代码
if (b) a = c;
else a = d;
尝试用以下代码替换它,它等效但可能更快
static const type lookup_table[] = { d, c };
a = lookup_table[b];
条件表达式被编译成分支。如果此类分支未被很好地预测,它将比查找表花费更长时间。
此技术也可以应用于一系列条件表达式。例如,不要使用以下代码
a = b1 ? c : b2 ? d : b3 ? e : f;
这等效于以下代码
if (b1) a = c;
else if (b2) a = d;
else if (b3) a = e;
else a = f;
尝试查看以下代码是否更快
static const type lookup_table[] = { f, e, d, d, c, c, c, c };
a = lookup_table[b1 * 4 + b2 * 2 + b3];
尝试在您需要访问引用对象之前稍微计算一下指针或迭代器的值。
例如,在循环中,以下语句
a = *++p;
可能比以下语句效率略低
a = *p++;
在第一种情况下,指针或迭代器的值在用于访问引用对象之前才计算,而在第二种情况下,它是在前一次迭代中计算的。在流水线处理器中,在第二种情况下,指针的增量可以与访问引用对象同时执行,而在第一种情况下,这两个操作必须序列化。