最便宜的方法来强制隐式数组边界检查

时间:2013-01-03 22:58:20

标签: java arrays performance jvm micro-optimization

假设我有一个带有以下签名的方法:

public int indexOf(byte[] bytes, byte toFind, int offset, int length) {
  ...
}

此方法执行简单操作,例如在toFind中查找[offset,offset + length]范围内的字节bytes。我想预先检查偏移量和长度是否对bytes有效。也就是说,偏移量和偏移量+长度以字节为单位。

显式检查看起来像:

if (offset < 0 || offset > bytes.length - length) {
  throw ...;  // bad santa!
}

似乎我可以通过执行“虚拟”数组访问来更便宜地执行此操作(就发出的字节码而言,可能还有运行时性能):

public int indexOf(byte[] bytes, byte toFind, int offset, int length) {
  int dummy = bytes[offset] + bytes[offset + length - 1];
  ...
}

如果可以的话,我想摆脱int dummy+,或者降低成本。编译器不喜欢像bytes[offset];这样的独立访问,大概是因为像这样的表达式通常没有副作用而且没有意义(但在这种情况下不是这样)。使用dummy int也会导致必须抑制的编译器警告。

关于如何使用最少的字节码进行更改的任何建议(运行时性能在这里也很重要,但我怀疑大多数解决方案都被优化为与丢弃未使用部分相同的东西)。

4 个答案:

答案 0 :(得分:2)

这个怎么样?

if ((bytes[offset] | bytes[offset+length-1])==0) { }

答案 1 :(得分:1)

我希望只执行bytes[offset]bytes[offset + length - 1]是最便宜的方式。 JVM字节码中最短的方法就是执行这些表达式并将其保留在操作数堆栈上。

但是,你不能用Java做到这一点。您也不能使用pop2指令(或两个pop指令),因为bytes[something]不是有效的Java命令。有三种可能的最佳方式:

  1. 使用int java.lang.Math.max(int, int)之类的方法调用。这会添加一条3字节invokestatic指令和一条1字节pop指令。因此,它是一个4字节的开销。如果编写带有两个int参数和void结果的静态虚拟方法,则可以保存一个字节。智能JVM优化器可能会将此代码减少为一个pop2指令,因为Math.max(...)没有副作用,您通过pop指令丢弃结果。但是,我不确定这是否适用于Hotspot。
  2. 将其分配给本地变量。一个赋值意味着一个istore指令。如果您有五个参数(包括this,因为该方法不是静态的),则使用通用的2字节istore版本而不是1字节istore_<n>(对于{0中的n) ,1,2,3})。如果你最多有三个参数,你可能会通过减少虚拟变量的范围来节省一些东西。
  3. 比较它(=&gt;生成布尔值)并使用空分支,即if ((bytes[offset] == bytes[offset+length-1])) { }。在这种情况下,您不需要任何额外的方法(如max或pop2)或任何额外的局部变量(放大局部变量表)。
  4. 如果你不使用任何进一步的优化器,并且你没有修改方法签名以使用更少的变量,第三种方式可能是赢家。在我的简单测试中,它只需要16个字节的指令(其他一些实现是相同的,但不是更好),并且在局部变量表或常量池中不需要任何其他内容。您可以通过手动字节码优化或Proguard来保存几个字节。但要小心,Proguard可能会对它进行过多优化并删除阵列访问权限。 (我不确定,但它在文档中声称它可能会删除一些NullPointerExceptions。)

    请参阅https://gist.github.com/4523924

答案 2 :(得分:1)

字节码长度方面最便宜的方式是几种JRE类使用的方式,例如: ByteBufferArrayList使用专门的检查方法

从Java 9开始,有一个标准方法用于此目的,它也开始取代这些内部检查方法,成为这种检查的中心位置,如果有一种方法,JVM的优化器也知道这种方法。优化此检查:
Objects.checkFromIndexSize​(offset, length, bytes.length);

与其他方法相比:

  • 使用带虚拟变量的数组访问:

    public int indexOf1(byte[] bytes, byte toFind, int offset, int length) {
        int dummy = bytes[offset] + bytes[offset + length - 1];
        //...
    }
    

    编译到

     0: aload_1
     1: iload_3
     2: baload
     3: aload_1
     4: iload_3
     5: iload         4
     7: iadd
     8: iconst_1
     9: isub
    10: baload
    11: iadd
    12: istore        5
    14: ...
    
  • 使用数组访问和虚拟分支指令

    public int indexOf2(byte[] bytes, byte toFind, int offset, int length) {
        if ((bytes[offset] | bytes[offset+length-1])==0) { }
        //...
    }
    

    编译到

     0: aload_1
     1: iload_3
     2: baload
     3: aload_1
     4: iload_3
     5: iload         4
     7: iadd
     8: iconst_1
     9: isub
    10: baload
    11: ior
    12: ifne          15
    15: ...
    
  • 使用专门的检查方法

    public int indexOf3(byte[] bytes, byte toFind, int offset, int length) {
        checkIndex(bytes, offset, length);
        //...
    }
    
    private void checkIndex(byte[] bytes, int offset, int length) {
       //...
    }
    

    编译到

     0: aload_0
     1: aload_1
     2: iload_3
     3: iload         4
     5: invokespecial #23                 // Method checkIndex:([BII)V
     8: ...
    

因此在考虑使用它的代码时委派胜利。它也不会在调用者端引入局部变量。一旦多个方法使用它,实现方法所需的额外空间就会得到回报。这样的方法通常是privatestatic,由没有动态调度的指令调用并在运行时进行内联,因此不存在性能差异。 JVM通常会内联小的方法,无论它们是否是热点。

当将隐式数组边界检查的性能与显式比较进行比较时,没有理由认为它们应该比另一个更快。它们基本上都是这样做的,在任何一种情况下,如果JVM可以证明调用者总是只传递有效数字,那么JVM可以忽略它们。

顺便说一下,Buffer.checkBounds引导您进入只有一个条件的实现:

private void checkIndex(byte[] bytes, int offset, int length) {
    if((offset | length | (offset+length) | (bytes.length-(offset+length))) < 0)
        throw new IndexOutOfBoundsException();
}

与数组访问变体不同,这也处理length为负数的情况(但offset+length将产生有效索引)。

答案 3 :(得分:0)

不确定字节码长度,但是如何:

bytes[offset] |= bytes[offset];
bytes[offset + length - 1] |= bytes[offset + length - 1];