gcc 是不是会根据条件优化我的周期?
Posted
技术标签:
【中文标题】gcc 是不是会根据条件优化我的周期?【英文标题】:Does gcc optimize my cycle with condition?gcc 是否会根据条件优化我的周期? 【发布时间】:2011-04-02 11:38:33 【问题描述】:我有以下循环:
//condition will be set here to true or false
for (int i = 0; i < LARGE_NUMBER; i++)
if (condition)
//do foo
else
//do bar
假设:如果没有条件比有条件更快,则循环。 (这是真的?)
问题:如果 condition
已设置在 for 循环之外,并且循环本身不接触 condition
,gcc 会排除我的 if
吗?
如果不是,我应该切换if
和for
,重复代码,违反DRY等
【问题讨论】:
如果'条件'没有改变,你为什么要把它放在'for'里面? @nacho4d:为了避免代码重复(for
标头,以及在if
之外但在for
内部的其他语句)
您是否将condition
声明为volatile
?
@mvds: 条件不是易变的,否则编译器无法预测它的值。
我对答案非常满意,+1,谢谢大家。
【参考方案1】:
对于那些不想阅读冗长文章的人,这种优化称为(在 LLVM 中)Loop Unswitch。
为什么不问编译器?
void foo(char* c);
int main(int argc, char **argv)
bool const condition = argc % 2;
for (int i = 0; i != argc; ++i)
if (condition)
foo(argv[1]);
else
foo(argv[0]);
return 0;
转化为SSA形式(通过LLVM try out):
define i32 @main(i32 %argc, i8** nocapture %argv)
entry:
%0 = icmp eq i32 %argc, 0 ; <i1> [#uses=1]
br i1 %0, label %bb5, label %bb.nph
bb.nph: ; preds = %entry
%1 = and i32 %argc, 1 ; <i32> [#uses=1]
%toBool = icmp eq i32 %1, 0 ; <i1> [#uses=1]
%2 = getelementptr inbounds i8** %argv, i64 1 ; <i8**> [#uses=1]
br i1 %toBool, label %bb3.us, label %bb3
bb3.us: ; preds = %bb3.us, %bb.nph
%i.07.us = phi i32 [ %4, %bb3.us ], [ 0, %bb.nph ] ; <i32> [#uses=1]
%3 = load i8** %argv, align 8 ; <i8*> [#uses=1]
tail call void @_Z3fooPc(i8* %3)
%4 = add nsw i32 %i.07.us, 1 ; <i32> [#uses=2]
%exitcond = icmp eq i32 %4, %argc ; <i1> [#uses=1]
br i1 %exitcond, label %bb5, label %bb3.us
bb3: ; preds = %bb3, %bb.nph
%i.07 = phi i32 [ %6, %bb3 ], [ 0, %bb.nph ] ; <i32> [#uses=1]
%5 = load i8** %2, align 8 ; <i8*> [#uses=1]
tail call void @_Z3fooPc(i8* %5)
%6 = add nsw i32 %i.07, 1 ; <i32> [#uses=2]
%exitcond8 = icmp eq i32 %6, %argc ; <i1> [#uses=1]
br i1 %exitcond8, label %bb5, label %bb3
bb5: ; preds = %bb3, %bb3.us, %entry
ret i32 0
也许不太可读,所以让我指出这里的内容:
entry
:检查argc
是否等于0,如果是,则转到bb5
(退出)否则转到bb.nph
bb.nph
:计算condition
的值,如果为真,则转至bb3.us
,否则转至bb3
bb3.us
和 bb3
:分别循环判断真假条件
bb5
:退出
只要效果与您所要求的相似,编译器几乎可以将您的代码转换成它想要的样子。在这种情况下,它有效地将代码重写为:
int main(int argc, char**argv)
if (argc != 0)
int i = 0;
if (argc % 2)
do
foo(argv[1]);
++i;
while (i != argc);
else
do
foo(argv[0]);
++i;
while (i != argc);
return 0;
这是循环不变优化的一种形式,在这里结合第一次检查以避免在循环不会被执行时计算条件。
对于我们这些认为第一个解决方案更清晰的人来说,我们很高兴让编译器为我们进行细节优化!
【讨论】:
快速说明 事实上,如果编译器进入循环展开(以避免检查它是否必须在每次迭代中保持循环),它可能会变得更加丑陋。如果您想查看手动展开,请查看 Duff 的设备,您可能会同意编译器执行此操作会更好。【参考方案2】:如果可以证明condition
在迭代期间不会发生变化,那么任何体面的优化编译器都会这样做。
此外,即使编译器实际上并没有这样做,您最好支持您的决定,即使用来自分析的硬数据将代码重写为人类可读性较差的形式。 不要过早优化。 给代码的读者一个“嗯?”是不值得的。为了缩短几毫秒的时间(“读者”肯定包括未来的你自己)。
【讨论】:
【参考方案3】:我不主张在这里采取任何行动,通过通常的“过早优化”论点。保持代码清晰最重要,如果整个程序太慢,您可能需要在程序彻底调试后分析并找出实际的瓶颈(通常无法猜测)。
即使编译器没有为您优化这种特殊情况,您也可能想知道 CPU 会执行某种形式的 branch prediction,这将大大减少在条件可预测的情况下处理条件所需的时间。
事实上,大多数 CPU 处理指令都在 pipeline 中,并且在必须确定跳转地址时,条件变量可能是未知的。这会导致管道停顿,这是大多数现代处理器试图猜测(实际上是聪明地)程序将跳转到哪里的地方。如果条件变量确实是已知的(就像您的情况一样),那么猜测将是完美的。
所以我怀疑即使使用“愚蠢”的编译器,您实际上会看到这两个选项之间的差异,至少在现代机器上是这样。
【讨论】:
其实大部分编译器应该在这里写两个循环,根据条件的值,如果可以证明条件在循环执行过程中没有改变。它不会使您所说的无效。 完全同意马修。 +1 用于分支预测,在这种情况下确实使讨论变得毫无意义。以上是关于gcc 是不是会根据条件优化我的周期?的主要内容,如果未能解决你的问题,请参考以下文章
如果我优化大小而不是速度,为什么GCC会生成15-20%的代码?
为啥在 GCC 5.1 中仍然启用 COW std::string 优化?