禁用时Java断言的性能拖累 [英] Performance drag of Java assertions when disabled

查看:124
本文介绍了禁用时Java断言的性能拖累的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

代码可以使用其中的断言进行编译,并且可以是在需要时激活/停用

Code can be compiled with assertions in it and can be activated/deactivated when needed.

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

But if I deploy an app with assertions in it and those are disabled what is the penalty involved in therm being there and ignored?

推荐答案

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

Contrary to the conventional wisdom, asserts do have a runtime impact and may affect performance. This impact is likely to be small in most cases but could be large in certain circumstances. Some of the mechanisms by which asserts slow things down at runtime are fairly "smooth" and predictable (and generally small), but the last way discussed below (failure to inline) is tricky because it is the largest potential issue (you could have an order-of-magnitude regression) and it isn't smooth1.

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

When it comes to analyzing the assert functionality in Java, a nice thing is that they aren't anything magic at the bytecode/JVM level. That is, they are implemented in the .class file using standard Java mechanics at (.java file) compile time, and they don't get any special treatment by the JVM2, but rely on the usual optimizations that apply to any runtime compiled code.

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

Let's take a quick look at exactly how they are implemented on a modern Oracle 8 JDK (but AFAIK it hasn't changed in pretty much forever).

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

Take the following method with a single assert:

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

...编译该方法并用 javap -c foo.bar.Main :

... compile that method and decompile the bytecode with 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()对象,如果它们失败。

The first 22 bytes of bytecode are all associated with the assert. Right up front, it checks the hidden static $assertionsDisabled field and jumps over all the assert logic if it is true. Otherwise, it just does the two checks in the usual way, and constructs and throws an AssertionError() object if they fail.

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

So there is nothing really special about assert support at the bytecode level - the only trick is the $assertionsDisabled field, which - using the same javap output - we can see is a static final initialized at class init time:

  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

因此编译器创建了这个隐藏的静态最终字段并根据公共<加载它< a href =https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#desiredAssertionStatus-- =nofollow noreferrer> desiredAssertionStatus() 方法。

So the compiler has created this hidden static final field and loads it based on the public desiredAssertionStatus() method.

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

So nothing magic at all. In fact, let's try to do the same thing ourselves, with our own static SKIP_CHECKS field that we load based on a system property:

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语句,但我们会尝试尽可能地匹配断言)。让我们检查输出:

Here we just write out longhand what the assertion is doing (we could even combine the if statements, but we'll try to match the assert as closely as possible). Let's check the output:

 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

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

Huh, it's pretty much byte-for-byte identical to the assert version.

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

So we can pretty much reduce the "how expensive is an assert" question to "how expensive is a code jumped over by an always-taken branch based on a static final condition?". The good news then is that such branches are generally completely optimized away by the C2 compiler, if the method is compiled. Of course, even in that case, you still pay some costs:


  1. 类文件较大,JIT代码较多。

  2. 在JIT之前,解释版本的运行速度可能会变慢。

  3. 函数的完整大小用于内联决策,因此存在断言即使在禁用时也会影响此决定。

  1. The class files are larger, and there is more code to JIT.
  2. Prior to JIT, the interpreted version will likely run slower.
  3. The full size of the function is used in inlining decisions, and so the presence of asserts affects this decision even when disabled.

积分(1)和(2)是直接的断言在运行时编译(JIT)期间被删除的结果,而不是在java文件编译时。这是与C和C ++断言的关键区别(但作为交换,您决定在每次启动二进制时使用断言,而不是在该决定中进行编译)。

Points (1) and (2) are a direct consequence of the assert being removed during runtime compile (JIT), rather than at java-file-compile time. This is a key difference with C and C++ asserts (but in exchange you get to decide to use asserts on each launch of the binary, rather than compiling in that decision).

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

Point (3) is probably the most critical, and is rarely mentioned and is hard to analyze. The basic idea is that the JIT uses a couple size thresholds when making inlining decisions - one small threshold (~30 bytes) under which it almost always inlines, and another larger threshold (~300 bytes) over which it never inlines. Between the thresholds, whether it inlines or not depends on whether the method is hot or not, and other heuristics such as whether it has already been inlined elsewhere.

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

Since the thresholds are based on the bytecode size, the use of asserts can dramatically affect those decisions - in the example above, fully 22 of the 26 bytes in the function were assert related. Especially when using many small methods, it is easy for asserts to push a method over the inlining thresholds. Now the thresholds are just heuristics, so it's possible that changing a method from inline to not-inline could improve performance in some cases - but in general you want more rather than less inlining since it is a grand-daddy optimization that allows many more once it occurs.

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

One approach to work around this issue is to move most of the assert logic to a special function, as follows:

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编译为无操作。

... and so has reduced the size of that function from 26 to 9 bytes, of which 5 are assert related. Of course, the missing bytecode has just moved to the other function, but that's fine because it will be considered separately in inlining decisions and JIT-compiles to a no-op when asserts are disabled.

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

Finally, it's worth noting that you can get C/C++-like compile-time asserts if you want. These are asserts whose on/off status is statically compiled into the binary (at javac time). If you want to enable asserts, you need a new binary. On the other hand, this type of assert is truly free at runtime.

如果我们更改自制SKIP_CHECKS 静态最终在编译时知道,如下所示:

If we change the homebrew SKIP_CHECKS static final to be known at compile time, like so:

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类,可以使整个项目更加可行,并且可以利用现有的断言糖制作1行版本:

That is, there is no trace left of the assert. In this case we can truly say there is zero runtime cost. You could make this more workable across a project by having a single StaticAssert class that wraps the SKIP_CHECKS variable, and you can leverage this existing assert sugar to make a 1-line version:

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

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

Again, this compiles at javac time down to bytecode without a trace of the assert. You are going to have to deal with an IDE warning about dead code though (at least in eclipse).

1 这个,我的意思是这个问题可能没什么影响,然后再对周围代码进行小规模无害的更改可能会突然产生很大影响。基本上,由于内联或不内联决策的二元效应,各种惩罚级别严重量化

1 By this, I mean that this issue may have zero effect, and then after a small innocuous change to surrounding code it may suddenly have a big effect. Basically the various penalty levels are heavily quantized due to the binary effect of the "to inline or not to inline" decisions.

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

2 At least for the all-important part of compiling/running the assert-related code at runtime. Of course there is a small amount of support in the JVM for accepting the -ea command line argument and flipping the default assertion status (but as above you can accomplish the same effect in a generic way with properties).

这篇关于禁用时Java断言的性能拖累的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆