为什么HotspotJIT不为长计数器执行循环展开?
我刚刚阅读了 Java Magazine 文章Loop Unrolling。作者在那里演示了for带有int计数器的简单循环是通过循环展开优化编译的:
private long intStride1()
{
long sum = 0;
for (int i = 0; i < MAX; i += 1)
{
sum += data[i];
}
return sum;
}
但是,他们随后通过将计数器类型切换为 来显示一切都发生了变化long:
private long longStride1()
{
long sum = 0;
for (long l = 0; l < MAX; l++)
{
sum += data[(int) l];
}
return sum;
}
这会通过以下方式更改输出程序集:
- 介绍安全点
- 不执行展开
这会显着降低吞吐量性能。
为什么 64 位 HotSpot VM 不为long计数器执行循环展开?为什么第二种情况需要安全点,而第一种情况不需要?
回答
从 JDK 16 开始,HotSpot JVM 支持循环展开和对具有 64 位计数器的循环进行其他优化。
JDK-8223051的描述回答了您的两个问题:
许多核心循环转换适用于计数循环,即具有计算行程计数的循环。转换包括展开、迭代范围拆分(数组 RCE)和条带挖掘(JDK-8186027)。优化器执行许多复杂的模式匹配来检测和转换计数循环。
大多数或所有这些模式匹配和转换适用于具有 32 位控制变量和算术的循环。只要批量操作仅适用于 Java 数组,这就是有意义的,因为这些数组只能跨越 31 位索引范围。用于大块数据块的较新 API 将引入 64 位索引,例如巴拿马的本机数组和(可能)范围扩展的字节缓冲区。在幕后,Unsafe API 通常使用 64 位地址和地址算法。处理此类数据结构的循环自然使用 64 位值,或者作为直接的 Java longs,或者作为带有递增 long 组件(巴拿马指针)的包装游标结构。
需要有一个故事来转换这种长时间运行的循环。这个 RFE 是对那个故事的请求。
一个复杂的因素是,有时计数循环没有安全点,假设最大可能的迭代(跨越 32 位动态范围)不会导致 JVM 的安全点机制由于无响应线程卡在这样的计数循环中而出现故障环形。该假设在 64 位情况下无效。幸运的是,我们有一个(相对较新的)优化可以解决这个问题,通过将单个非常长的运行循环剥离成具有适当限制行程计数的循环序列(外循环)。