禁用时Java断言的性能拖累

时间:2011-01-07 11:17:02

标签: java performance assertions

代码可以使用其中的断言进行编译,并且可以是activated/deactivated when needed

但是如果我在其中部署一个带有断言的应用程序并且那些被禁用,那么在那里被忽略的惩罚会被忽略吗?

3 个答案:

答案 0 :(得分:27)

与传统观点相反,断言确实会对运行时产生影响并可能影响性能。在大多数情况下,此影响可能很小,但在某些情况下可能会很大。断言在运行时减慢事情的一些机制是相当平稳的#34;和可预测的(通常很小),但下面讨论的最后一种方式(内联失败)是棘手的,因为它是最大的潜在问题(你可能有一个数量级的回归),它不是光滑 1

分析

断言实施

在分析Java中的assert功能时,一个好处是它们在字节码/ JVM级别上没有什么神奇之处。也就是说,它们在.class文件中使用标准Java机制在(.java文件)编译时实现,并且他们没有得到JVM 2 的任何特殊处理,但依赖于适用于任何运行时编译代码的常规优化。

让我们快速浏览完全如何在现代Oracle 8 JDK上实现它们(但AFAIK它几乎没有永远改变)。

使用单个断言采用以下方法:

public int addAssert(int x, int y) {
    assert x > 0 && y > 0;
    return x + y;
} 

...编译该方法并使用javap -c foo.bar.Main反编译字节码:

  public int addAssert(int, int);
    Code:
       0: getstatic     #17                 // Field $assertionsDisabled:Z
       3: ifne          22
       6: iload_1
       7: ifle          14
      10: iload_2
      11: ifgt          22
      14: new           #39                 // class java/lang/AssertionError
      17: dup
      18: invokespecial #41                 // Method java/lang/AssertionError."<init>":()V
      21: athrow
      22: iload_1
      23: iload_2
      24: iadd
      25: ireturn

字节码的前22个字节都与断言相关联。在前面,它检查隐藏的静态$assertionsDisabled字段并跳过所有断言逻辑(如果它是真的)。否则,它只是以通常的方式执行两次检查,并构造并抛出AssertionError()对象,如果它们失败。

因此,字节码级别的断言支持没有什么特别之处 - 唯一的技巧是$assertionsDisabled字段,使用相同的javap输出 - 我们可以看到static final在初始化时初始化:

  static final boolean $assertionsDisabled;

  static {};
    Code:
       0: ldc           #1                  // class foo/Scrap
       2: invokevirtual #11                 // Method java/lang/Class.desiredAssertionStatus:()Z
       5: ifne          12
       8: iconst_1
       9: goto          13
      12: iconst_0
      13: putstatic     #17                 // Field $assertionsDisabled:Z

因此,编译器已创建此隐藏的static final字段,并根据公共desiredAssertionStatus()方法加载它。

所以没有任何魔法。实际上,让我们自己做同样的事情,使用我们自己的基于系统属性加载的静态SKIP_CHECKS字段:

public static final boolean SKIP_CHECKS = Boolean.getBoolean("skip.checks");

public int addHomebrew(int x, int y) {
    if (!SKIP_CHECKS) {
        if (!(x > 0 && y > 0)) {
            throw new AssertionError();
        }
    }
    return x + y;
}

这里我们只是简单地写出断言正在做什么(我们甚至可以组合if语句,但我们会尝试尽可能地匹配断言)。让我们检查一下输出:

 public int addHomebrew(int, int);
    Code:
       0: getstatic     #18                 // Field SKIP_CHECKS:Z
       3: ifne          22
       6: iload_1
       7: ifle          14
      10: iload_2
      11: ifgt          22
      14: new           #33                 // class java/lang/AssertionError
      17: dup
      18: invokespecial #35                 // Method java/lang/AssertionError."<init>":()V
      21: athrow
      22: iload_1
      23: iload_2
      24: iadd
      25: ireturn

嗯,它与字节版本的字节完全相同。

断言成本

所以我们几乎可以减少&#34;断言的成本是多少?问题是&#34;基于static final条件的始终采用分支跳过的代码有多贵?&#34;。好消息是,这些分支通常由C2编译器完全优化,如果编译该方法。当然,即使在这种情况下,您仍需支付一些费用:

  1. 类文件较大,JIT有更多代码。
  2. 在JIT之前,解释版本可能会运行得更慢。
  3. 函数的完整大小用于内联决策,因此即使禁用,断言的存在也会影响此决策。
  4. 点(1)和(2)是在运行时编译(JIT)期间删除断言的直接结果,而不是在java文件编译时。这是与C和C ++断言的关键区别(但作为交换,您可以决定在每次启动二进制时使用断言,而不是在该决策中进行编译)。

    第(3)点可能是最关键的,很少被提及,很难分析。基本思想是JIT在进行内联决策时使用几个大小的阈值 - 一个小阈值(~30个字节),它几乎总是内联,另一个更大的阈值(~300个字节),它从不内联。在阈值之间,是否内联取决于方法是否热,以及其他启发式方法,例如是否已在其他地方内联。

    由于阈值是基于字节码大小的,因此使用断言可以显着影响这些决策 - 在上面的示例中,函数中26个字节中的22个完全与断言相关。特别是当使用许多小方法时,断言很容易将方法推到内联阈值上。现在阈值只是启发式,因此将某个方法从内联更改为非内联可能会在某些情况下提高性能 - 但总的来说,您需要更多而不是更少的内联,因为它是一个祖父优化,一旦发生就允许更多。

    缓解

    解决此问题的一种方法是将大多数断言逻辑移动到特殊函数,如下所示:

    public int addAssertOutOfLine(int x, int y) {
        assertInRange(x,y);
        return x + y;
    }
    
    private static void assertInRange(int x, int y) {
        assert x > 0 && y > 0;
    }
    

    这编译为:

      public int addAssertOutOfLine(int, int);
        Code:
           0: iload_1
           1: iload_2
           2: invokestatic  #46                 // Method assertInRange:(II)V
           5: iload_1
           6: iload_2
           7: iadd
           8: ireturn
    

    ...因此将该函数的大小从26减少到9个字节,其中5个与断言相关。当然,丢失的字节码刚刚转移到另一个函数,但这很好,因为它会在内联决策时被单独考虑,而当断言被禁用时,JIT编译成无操作。

    True Compile-Time Asserts

    最后,值得注意的是,如果需要,可以获得C / C ++ - 就像编译时断言一样。这些是断言,其开/关状态被静态编译为二进制(在javac时间)。如果要启用断言,则需要新的二进制文件。另一方面,这种类型的断言在运行时是真正自由的。

    如果我们将自制的SKIP_CHECKS static final更改为在编译时知道,如下所示:

    public static final boolean SKIP_CHECKS = true;
    

    然后addHomebrew编译为:

      public int addHomebrew(int, int);
    Code:
       0: iload_1
       1: iload_2
       2: iadd
       3: ireturn
    

    也就是说,断言没有留下任何痕迹。在这种情况下,我们可以真正地说运行成本为零。通过使用包含SKIP_CHECKS变量的单个StaticAssert类,您可以使整个项目更加可行,并且可以利用现有的assert糖制作1行版本:

    public int addHomebrew2(int x, int y) {
        assert SKIP_CHECKS || (x > 0 && y > 0);
        return x + y;
    }
    

    同样,这会将在javac时间编译为字节码而没有断言的痕迹。您将不得不处理关于死代码的IDE警告(至少在eclipse中)。

    1 通过这个,我的意思是这个问题可能没有效果,然后在对周围代码进行一次小的无害改变后,它可能会突然产生很大的影响。基本上,各种惩罚级别都是量化的,因为&#34;内联或不内联的二进制效应&#34;决定。

    2 至少在运行时编译/运行与断言相关的代码的重要部分。当然,JVM中有少量支持接受-ea命令行参数并翻转默认断言状态(但如上所述,您可以通过属性以通用方式实现相同的效果)。

答案 1 :(得分:2)

很少。我相信他们会在上课时被删除。

我最接近的证据是:Java语言规范中的The assert statement specification。似乎措辞如此,以便可以在类加载时处理断言语句。

答案 2 :(得分:1)

  

禁用断言会消除它们   性能惩罚完全。一旦   残疾人,他们本质上   相当于空语句   语义和性能

Source