【问题标题】:Cheapest way to force implicit array bounds check强制隐式数组边界检查的最便宜方法
【发布时间】:2013-01-03 22:58:19
【问题描述】:

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

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

这个方法做了一些简单的事情,比如在bytes 的[offset, offset+length) 范围内查找字节toFind。我想预先检查偏移量和长度是否对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 也会导致编译器警告,必须禁止该警告。

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

【问题讨论】:

  • 以下形式的评论:“你分析过这个吗?”、“这真的是你的应用程序的瓶颈吗”、“几个字节/纳秒在这里很重要”、“闻起来像过早优化”将是忽略。
  • 可以直接操作字节码吗?还是您仅限于发出 Java 代码?
  • @templatetypedef - 仅限 .java 代码。
  • 您是否要尽量减少生成的字节码?或者最小化运行时间?
  • @BeeOnRope 请注意,您的 2 个示例并不等效。特别是,例如,对于 offset = 3 和 length = -1,第一个会引发异常,而第二个不会。

标签: java arrays performance jvm micro-optimization


【解决方案1】:

这个怎么样?

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

【讨论】:

  • 关于指令大小,这比问题的原始解决方案还要长。在考虑局部变量的元信息时,只需在编译时禁用调试信息就会删除它们。当代码大小成为问题时,关闭调试信息将是第一步。
  • @Holger - 关于本地人的任何元信息都与实际字节码不符,对吧?所以禁用它不会使字节码更小,这是我在这里的主旨(例如,因为字节码大小在内联决策中至关重要)。
  • @BeeOnRope:完全正确。在考虑内联决策时,此答案的代码仅比您问题的代码大一个字节。请注意,v6ak 的答案略有不同,使用 bytes[offset] == bytes[offset+length-1] 而不是 (bytes[offset] | bytes[offset+length-1])==0。这是小一个字节,因此与您的原始代码一样大。请注意,当将检查移动到另一个方法并与其他方法共享时,它变得“热”的可能性更大,而对于热方法,内联阈值(很多)更大,从而提高了内联的机会。跨度>
  • 是的,我仍然没有关于内联启发式的完整心智模型。我知道正常和热方法的两个阈值(30 和 300 字节,IIRC),但我不明白,例如,热度是否与调用者-被调用者对有关,或者仅取决于被调用者等
【解决方案2】:

我希望只执行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_&lt;n&gt;(对于 {0、1、2、3 中的 n })。如果您最多有三个参数,您可能会通过缩小虚拟变量的范围来节省一些东西。
  3. 比较它(=> 生成布尔值)并使用一个空分支,即if ((bytes[offset] == bytes[offset+length-1])) { }。在这种情况下,您不需要任何额外的方法(如 max 或 pop2)或任何额外的局部变量(扩大局部变量表)。

如果您不使用任何进一步的优化器并且您不修改方法签名以使用更少的变量,那么第三种方式可能是赢家。在我的简单测试中,它只需要 16 个字节的指令(其他一些实现是相同的,但不是更好),并且在局部变量表或常量池中不需要更多内容。您可能可以通过手动字节码优化或 Proguard 节省几个字节。但要小心,Proguard 可能会对其进行过多的优化并删除数组访问。 (我不确定,但它在文档中声称它可能会删除一些 NullPointerExceptions。)

https://gist.github.com/4523924

【讨论】:

    【解决方案3】:

    就字节码长度而言,最便宜的方式是多个 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 可以证明调用者总是只传递有效数字,它就可以忽略它们。

    顺便说一句,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 会产生有效索引)。

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

      • 为什么不bytes[offset] |= 0; bytes[offset + length - 1] |= 0;?这仍然比其他方法长,但比重复数组访问表达式要短得多。
      猜你喜欢
      • 2023-04-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-26
      • 2011-01-21
      • 1970-01-01
      相关资源
      最近更新 更多