为什么边界检查没有被消除?答案摘要新闻
我写了一个简单的基准测试,以便找出当数组通过按位和计算时是否可以消除边界检查。这基本上就是几乎所有哈希表的作用:它们计算
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
- 对于每个数组访问,检查索引是否通过按位和计算。
- 如果是这样,请检查是否将任一操作数计算为长度减去 1。
- 如果是这样,请将边界检查替换为零长度检查。
- 让现有的优化来处理它。
这样的优化应该非常简单和便宜,因为它只查看SSA图中的父节点。与许多复杂的优化不同,它永远不会是有害的,因为它只能用稍微简单的检查替换一个检查;所以没有问题,即使它不能被移出循环。
我会把它发布到热点开发邮件列表。