热点编译器是否能够通过按位运算来限制索引范围,从而消除边界检查?

8

考虑以下函数:

int foo(int[] indices) {
  int[] lookup = new int[256];
  fill(lookup); // populate values, not shown

  int sum = 0;
  for (int i : indices) {
    sum += lookup[i & 0xFF]; // array access
  }

  return sum;
}

现代的HotSpot虚拟机能否消除对于`lookup[i & 0xFF]`访问的边界检查?这个访问不会越界,因为`i & 0xFF`在0-255的范围内,而数组有256个元素。

这不是编译器应该优化的吗?我认为HotSpot更多关注于针对“经常调用函数,让调用更快”的优化。 - akuzminykh
在Java中,编译器几乎没有进行任何优化。几乎所有标准的编译器优化都是由JIT在运行时应用的。这并不能说明它是否“应该”这样,只是实际上是如此。在这种情况下,我认为编译器无法进行优化:边界检查已经隐含在数组访问字节码中(与编译为本机代码的编译器不同,在访问之前,边界检查将使用一些显式指令):因此除了某种“不安全的数组访问”字节码外,只有JVM可以对其进行优化。 - BeeOnRope
1个回答

8

是的,这是一种相对简单的优化,HotSpot绝对可以实现。 JIT编译器推断表达式的可能范围,并使用这些信息来消除冗余检查。

我们可以通过打印汇编代码进行验证: -XX:CompileCommand = print,Test :: foo

...
0x0000020285b5e230: mov     r10d,dword ptr [rcx+r8*4+10h]  # load 'i' from indices array
0x0000020285b5e235: and     r10d,0ffh                      # i = i & 0xff
0x0000020285b5e23c: mov     r11,qword ptr [rsp+8h]         # load 'lookup' into r11
0x0000020285b5e241: add     eax,dword ptr [r11+r10*4+10h]  # eax += r11[i]

在加载 ilookup[i & 0xff] 之间,没有比较指令。


2
很惊讶看到r11的负载,它是不变的,所以它不应该移出循环吗? - amonakov
1
昨天用IGV看了一下,想知道为什么在解析后我没有看到范围检查,但事实上范围检查甚至从未被发出:https://github.com/openjdk/jdk/blob/master/src/hotspot/share/opto/parse2.cpp#L140-L144 另一种找到这个问题的方法是使用-XX:+LogComilation并查找<observe that='!need_range_check'/>对于iaload指令(可以将bci与javap输出匹配)。所以,这真的是一个非常简单的优化 :) - Jorn Vernee
@amonakov 确实,JDK 8 上存在冗余负载。当我在 JDK 11 上运行相同的代码时,它消失了(所以循环体只包含了3条指令)。 - apangin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接