h & (table.length - 1)
作为索引进入
table
,其中h
是hashCode
或派生值。results显示边界检查未被消除。我的基准测试的想法非常简单:计算两个值
i
和j
,其中两者都保证是有效的数组索引。
i
是循环计数器。当它用作数组索引时,边界检查会被消除。j
计算为x & (table.length - 1)
,其中x
是每次迭代时变化的某个值。当它用作数组索引时,边界检查不会被消除。
for (int i=0; i<=table.length-1; ++i) {
x += result;
final int j = x & (table.length-1);
result ^= i + table[j];
}
另一个实验使用
result ^= table[i] + j;
相比于“normal”代码,时间上的差异可能为15%(在我尝试的不同变体中相当一致)。我的问题:
- 除了边界检查消除之外,还有其他可能的原因吗?
- 有没有一些我看不到的复杂原因,导致不能消除
j
的边界检查?
答案摘要
MarkoTopolnik的答案表明,这一切都更加复杂,并且边界检查的消除并不能保证是胜利的,特别是在他的电脑上,“正常”代码比“掩码”慢。我想这是因为它允许一些额外的优化,但实际上在这种情况下是有害的(考虑到当前CPU的复杂性,编译器几乎无法确定)。
leventov的答案清楚地显示数组边界检查在“掩码”中得到执行,并且其消除使代码与“正常”速度一样快。
Donal Fellows指出,对于零长度表,掩码无效,因为x&(0-1)
等于x
。因此,编译器能做的最好的事情就是将边界检查替换为零长度检查。但是我认为这仍然值得,因为零长度检查可以轻松地移出循环。
优化建议
由于等价性a[x & (a.length - 1)]
仅在a.length == 0
时抛出,编译器可以执行以下操作:
- 对于每个数组访问,请检查索引是否通过按位与计算。
- 如果是这样,请检查任一操作数是否计算为减一后的长度。
- 如果是这样,请用零长度检查替换边界检查。
- 让现有优化处理它。
这种优化应该非常简单和便宜,因为它只查看SSA图中的父节点。与许多复杂的优化不同,它永远不会有害,因为它只用稍微简单的检查替换一个检查;所以就算无法将其移出循环,也没有问题。
我将此发布到hotspot-dev邮件列表。
-XX:CompileCommand = print,* Benchmark.time *
除了过滤掉您不感兴趣的内容外,还会给出更好的输出(例如,不显示实际寄存器名称的占位符)。 - Marko Topolnikx += 1
替换了x += i
,所以访问是顺序的,除了一个单独的环绕,但没有太多变化。我还尝试消除x
并设置j = i & (table.length-1)
,它等同于j = i
,但似乎防止了边界检查的消除。 - maaartinusx % (table.length-1)
而不是x & (table.length-1)
吗?也许编译器无法在编译时智能地计算出按位与的边界。 - SnakE