我编写了一个简单的基准测试,以确定在通过按位与计算数组时是否可以消除边界检查。这基本上是几乎所有哈希表所做的:它们计算
h & (table.length - 1)
作为 的索引table
,其中或h
是hashCode
派生值。结果表明边界检查没有被消除。
我的基准测试的想法非常简单:计算两个值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;
反而。时间上的差异可能是 15%(在我尝试过的不同变体中非常一致)。我的问题:
- 除了约束检查消除之外,还有其他可能的原因吗?
- 是否有一些复杂的原因我看不出为什么没有约束检查消除
j
?
答案摘要
MarkoTopolnik 的回答表明这一切都更加复杂,并且不能保证消除边界检查是胜利,尤其是在他的计算机上,“正常”代码比“屏蔽”代码慢。我想这是因为它允许进行一些额外的优化,这在这种情况下实际上是有害的(鉴于当前 CPU 的复杂性,编译器甚至几乎无法确定)。
leventov 的回答清楚地表明,数组边界检查是在“屏蔽”中完成的,并且它的消除使代码与“正常”一样快。
Donal Fellows 指出了这样一个事实,即屏蔽不适用于零长度表,x & (0-1)
如x
. 所以编译器能做的最好的事情就是用零长度检查代替边界检查。但恕我直言,这仍然值得,因为零长度检查可以轻松移出循环。
建议的优化
由于a[x & (a.length - 1)]
当且仅当 等价抛出a.length == 0
,编译器可以执行以下操作:
- 对于每个数组访问,检查索引是否已通过按位与计算。
- 如果是这样,请检查是否有任何一个操作数被计算为长度减一。
- 如果是这样,请将边界检查替换为零长度检查。
- 让现有的优化来处理它。
这样的优化应该非常简单且便宜,因为它只查看SSA图中的父节点。与许多复杂的优化不同,它永远不会有害,因为它只是用稍微简单的检查代替了一项检查;所以没有问题,即使它不能移出循环。
我会将其发布到热点开发邮件列表。