Ser*_*sly 1 c++ premature-optimization micro-optimization
我需要遍历大量(2D)数据,并且仅有时处理特殊情况。对于我的应用程序来说,速度是最关键的因素。
(我)很快想到的选择是:
选项A:
void ifInLoop(bool specialCase, MyClass &acc) {
for (auto i = 0; i < n; ++i) {
for (auto j = 0; j < n; ++j) {
if (specialCase) {
acc.foo();
} else {
acc.bar();
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
选项B:
void loopsInIf(bool specialCase, MyClass &acc) {
if (specialCase) {
for (auto i = 0; i < n; ++i) {
for (auto j = 0; j < n; ++j) {
acc.foo();
}
}
} else {
for (auto i = 0; i < n; ++i) {
for (auto j = 0; j < n; ++j) {
acc.bar();
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
选项C:
template <bool specialCase>
void templateIf(MyClass &acc) {
for (auto i = 0; i < n; ++i) {
for (auto j = 0; j < n; ++j) {
if (specialCase) {
acc.foo();
} else {
acc.bar();
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
我知道这属于过早的优化。但是,从理论的角度来看,当使用-O3(GCC / Clang)进行编译时,我会对这些代码片段在产生的组装和速度方面的差异感兴趣。
(Perl中已经存在与此类似的问题,但我想专门了解C ++。)
(编辑)specialCase在编译时已知吗?
并不是的。调用本身处于另一个循环中,对某些迭代的处理方式有所不同。所以类似(但不一定等距,但独立于用户输入):
for (int i = 0; i < m; ++i) {
ifInLoop(i % 10, acc);
}
Run Code Online (Sandbox Code Playgroud)
在这里我将如何使用选项C?如果有额外的介绍,因此我希望它与B非常相似。
for (int i = 0; i < m; ++i) {
if (i % 10)
templateIf<true>(acc);
else
templateIf<false>(acc);
}
Run Code Online (Sandbox Code Playgroud)
如果此函数可以内联到传递编译时常数的调用方中bool,那么您可以使用选项A(只要函数小到可以内联)。即,如果可以使用模板arg,则通常通常不需要。除了if强迫您编写if(var) { foo<true>(arg); }else {foo<false>(arg); }鼓励编译器使用2个版本的循环进行汇编时。
所有现代的编译器都足够聪明,可以内联小型函数,然后完全优化一个if(constant)。内联+常量传播使现代C ++可以高效地进行编译。
但是,如果在编译时不知道布尔值,则选项B可能更有效。 (如果该函数不经常运行,则从总体上看它的速度可能无关紧要,而差异可能很小。)
这是静态代码大小(I缓存占用空间)与动态指令数之间的权衡。或者,如果特殊情况很少运行,则该版本的循环可能在缓存中保持冷态。
Run Code Online (Sandbox Code Playgroud)for (int i = 0; i < m; ++i) { ifInLoop(i % 10, acc); }
如果确实有这样的重复模式,则编译器可能会决定为您展开此循环,以便bool成为编译时常量。
或者,如果编译器没有决定自己发明新的内部循环,则可以让编译器更好地进行汇编,而对于包含另一个完整循环的10个循环展开,对于编译器的试探法来说实在太多了。
int i;
for (i = 0; i < m-9; i+=10) { // potentially runs zero times if signed m <= 9
ifInLoop(false, acc); // this is the j=0
for (int j=1; j<10 ; j++) // j = i%10
ifInLoop(true, acc); // original i = i+j in case you need it
}
// cleanup loop:
for ( ; i < m ; i++) {
ifInLoop(i % 10, acc);
}
Run Code Online (Sandbox Code Playgroud)
如果编译器没有提升if并生成两个版本的循环,那么完美地预测不会消除检查分支条件的指令的前端+后端吞吐量成本。
如果编译器知道每次迭代仅运行或主体,则可能会进行重大的简化/ 优化ifelse,但是即使在运行时检查结果是否正确,分支仍会忽略这些优化,即使它预测的很好。
通常的“概要分析”堆栈溢出响应并不像大多数人认为的那样有用。首先,微基准测试很难。完全衡量错误的事情或得出荒谬的结论是很容易的,因为您对可能的问题和不重要的事情并不了解。(确保将您的CPU预热到最大turbo频率,然后首先初始化内存,这样您就不会将CoW映射到零页面,并且第一次定时传递不会支付页面错误和TLB遗失成本。优化编译启用,并检查效果是否与重复次数成线性比例。)
对一个测试用例进行分析并不能告诉您一般的代价。您错过了哪些优化,以及编译器是否愿意为您拆分循环并提升分支,取决于循环的详细信息(可能包括循环主体的复杂程度)。
唯一可以确定的方式就是使用您关心的编译器查看特定情况下的asm。
当然,不同的编译器(或同一编译器的不同版本,或者具有gcc -mtune=genericvs.之类的不同调整选项gcc -mtune=skylake)肯定会影响编译器是否决定将循环反转/分割以在两个循环之间选择一次。调整选项为此类决策设置启发式常量,并在静态代码大小与动态指令计数之间进行权衡的情况下展开循环。
部分原因可能取决于外部有多少工作,if()并且在拆分时必须原样复制。
| 归档时间: |
|
| 查看次数: |
158 次 |
| 最近记录: |