我正在阅读一篇关于Java实时编译器(JIT)优化技术的文档。其中之一是"循环反转"。文件上写着:
用
do-while
循环替换常规的while
循环。和CCD_ 3循环被设置在CCD_。这种替换可以减少两次跳跃。
循环反转是如何工作的,它如何优化我们的代码路径?
注意: 如果有人能用Java代码的例子来解释JIT如何将其优化为本机代码,以及为什么它在现代处理器中是最优的,那就太好了
while (condition) {
...
}
工作流程:
- 检查条件
- 如果为false,则跳到循环外
- 运行一次迭代
- 跳到顶端
if (condition) do {
...
} while (condition);
工作流程:
- 检查条件
- 如果为false,则跳到循环之外
- 运行一次迭代
- 检查条件
- 如果为true,则跳到步骤3
比较这两个,你可以很容易地看到,后者可能根本不会进行任何跳跃,前提是循环中只有一步,通常跳跃的次数会比迭代次数少一步。前者将不得不跳回来检查条件,只有在条件为false时才能跳出循环。
现代流水线CPU架构上的跳转可能非常昂贵:由于CPU在跳转之前完成了检查的执行,因此跳转之后的指令已经在流水线的中间。如果分支预测失败,则必须放弃所有这些处理。在重新安排管道时,将延迟进一步执行。
解释上述分支预测:对于每种条件跳转,CPU都有两个指令,每个指令都包括一个关于结果的赌注。例如,你会放一条指令,上面写着">如果不是零就跳,在非零上下注";在循环结束时,因为必须在除最后一次迭代之外的所有迭代上进行跳转。通过这种方式,CPU开始用跳转目标之后的指令而不是跳转指令本身之后的指令来泵送其流水线。
重要提示
请不要做将此作为如何在源代码级别进行优化的示例。这将是完全错误的,因为正如您的问题中已经清楚的那样,从第一种形式到第二种形式的转换是JIT编译器作为一种例程完全独立地完成的。
这可以优化总是至少执行一次的循环。
然后,一个常规的while
循环将总是跳回到起点至少一次,并在终点跳到终点一次。一个简单循环运行一次的例子:
int i = 0;
while (i++ < 1) {
//do something
}
另一方面,do-while
循环将跳过第一个和最后一个跳跃。这里有一个与上面的循环等效的循环,它将在没有跳跃的情况下运行:
int i = 0;
if (i++ < 1) {
do {
//do something
} while (i++ < 1);
}
让我们来看看它们:
while
版本:
void foo(int n) {
while (n < 10) {
use(n);
++n;
}
done();
}
- 首先我们测试
n
,如果条件不成立,则跳转到done();
- 然后我们使用并递增CCD_ 10
- 现在我们跳回到状态
- 冲洗,重复
- 当条件不再成立时,我们跳到
done()
do-while
版本:
(记住,我们实际上并没有在源代码中这样做[这会带来维护问题],编译器/JIT为我们这样做。)
void foo(int n) {
if (n < 10) {
do {
use(n);
++n;
}
while (n < 10);
}
done();
}
- 首先我们测试
n
,如果条件不成立,则跳转到done();
- 然后我们使用并递增CCD_ 15
- 现在我们测试一下情况,如果是真的,就跳回去
- 冲洗,重复
- 当条件不再成立时,我们流向(而不是跳转)
done()
例如,如果n
一开始是9
,我们在do-while
版本中根本不会跳,而在while
版本中,我们必须跳回开始,进行测试,然后当我们发现它不是真的时,跳回结束。
循环反转是一种性能优化技术,它可以提高性能,因为处理器可以用更少的指令实现相同的结果。这应该主要改善边界条件下的性能。
此链接提供了循环反转的另一个示例。在少数将递减和比较作为单个指令集实现的体系结构中,通过递减和比较操作将for循环转换为while是有意义的。
维基百科有一个很好的例子,我在这里再次解释。
int i, a[100];
i = 0;
while (i < 100) {
a[i] = 0;
i++;
}
将由编译器转换为
int i, a[100];
i = 0;
if (i < 100) {
do {
a[i] = 0;
i++;
} while (i < 100);
}
这如何转化为性能当i的值为99时,处理器不需要执行GOTO(在第一种情况下是必需的)。这提高了性能。