即时编译器针对循环程序块的编译优化,生成的IR图会改变原有的循环程序块内容 — 外提与展开。(外提就是提取公因式,展开就是减少判断次数)
循环优化一:无关代码外提 — 减少某些程序的重复执行
即时编译器会将常值放到循环体外,并且计算一次后会将这些常值放入缓存,每次循环直接从缓存中取数据。
int foo(int x, int y, int[] a) {
int sum = 0;
for (int i = 0; i < a.length; i++) {
sum += x * y + a[i];
}
return sum;
}
/** 等价于以下代码 */
int fooManualOpt(int x, int y, int[] a) {
int sum = 0;
int t0 = x * y; // x*y每次在循环体中都是不变的,放到循环外只需要计算一次就可以了
int t1 = a.length; // 数组的长度保存在数组对象的对象头中
for (int i = 0; i < t1; i++) {
sum += t0 + a[i];
}
return sum;
}
循环优化二:循环展开 — 减少for判断的次数
循环体不一定会按照既定的增长幅度进行(i++可能会变成i+=2),也就是会将多次循环合并到一次循环中执行(部分展开:具体合并几个循环得看即时编译器优化结果),当循环次数比较少时,可能会取消循环体(完全展开),直接顺序执行几条指令。
循环调用会消耗CPU资源(判断是否满足循环条件),循环展开会增加代码的冗余度(编译成机器码会变多),所以即时编译器会进行权衡选择合适的展开程度。
int foo(int[] a) {
int sum = 0;
for (int i = 0; i < 64; i++) {
sum += (i % 2 == 0) ? a[i] : -a[i];
}
return sum;
}
/** 优化完后如下 */
int foo(int[] a) {
int sum = 0;
for (int i = 0; i < 64; i += 2) { // 注意这里的步数是2
sum += (i % 2 == 0) ? a[i] : -a[i];
sum += ((i + 1) % 2 == 0) ? a[i + 1] : -a[i + 1];
}
return sum;
}
- 循环展开的条件:空间换时间
1. 循环下标数据类型是int、short、或者char;for(int i=0; i < max; i++ )
2. 循环上限是与循环无关的;for(int i=0; i < max; i++ )
3. 循环增长步长为固定值;for(int i=0; i < max; i++ )
循环优化三:循环判断外提 — 减少if判断次数
将循环体中的if程序块放到循环外(要保证不影响程序原语义)。
int foo(int[] a) {
int sum = 0;
for (int i = 0; i < a.length; i++) {
if (a.length > 4) {
sum += a[i];
}
}
return sum;
}
/** 优化完后如下 */
int foo(int[] a) {
int sum = 0;
if (a.length > 4) {
for (int i = 0; i < a.length; i++) {
sum += a[i];
}
} else {
for (int i = 0; i < a.length; i++) {
}
}
return sum;
}
// 进一步优化为:
int foo(int[] a) {
int sum = 0;
if (a.length > 4) {
for (int i = 0; i < a.length; i++) {
sum += a[i];
}
}
return sum;
}
循环优化四:循环剥离 — 尽量让循环体内处理的逻辑一致,没有特殊情况
将循环的首尾几次循环放到循环外,因为通常情况下,循环开始于结束位置我们会进行特殊的额外处理(比如边界处理),剥离后中间的循环体每次的逻辑都是一样的,可以提高执行效率。
int foo(int[] a) {
int j = 0;
int sum = 0;
for (int i = 0; i < a.length; i++) { // 将i=0提出
sum += a[j];
j = i;
}
return sum;
}
/** 优化完后如下 */
int foo(int[] a) {
int sum = 0;
if (0 < a.length) {
sum += a[0]; // 特殊操作第一次循环,提高循环体外面
for (int i = 1; i < a.length; i++) {
sum += a[i - 1];
}
}
return sum;
}
向量化优化
包括HotSpot intrinsic与自动向量化:主要就是指将多次对数据的操作指令放入到单次CPU操作中。也就是SIMD指令:基于大长度的寄存器实现一次操作能读写更多的数据(X86默认寄存器是64位,8字节,也就是一次读写数据最多8字节,但是现在有很多其他的支持更多字节的寄存器),所以CPU执行一次操作可以处理更多的指令。
结合上述循环展开,可以将多行循环体操作放到同一个CPU指令中执行。
参考文章:
https://time.geekbang.org/column/article/39814