代码可以用其中的断言进行编译,并且可以在需要时激活/停用。
但是,如果我部署了一个带有断言的应用程序并且这些断言被禁用,那么它们存在并被忽略的惩罚是什么?
代码可以用其中的断言进行编译,并且可以在需要时激活/停用。
但是,如果我部署了一个带有断言的应用程序并且这些断言被禁用,那么它们存在并被忽略的惩罚是什么?
与传统观点相反,断言确实会影响运行时并可能影响性能。平均而言,影响可能很小,在中位情况下可能为零,但当恒星正好对齐时,影响可能会很大。
在运行时断言减慢速度的一些机制是相当“平滑”和可预测的(通常很小),但下面讨论的最后一种方式(内联失败)是棘手的,因为它是最大的潜在问题(你可能有一个数量级回归)并且它不是平滑的1。
在分析assert
Java 中的功能时,一件好事是它们在字节码/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
嗯,它几乎与断言版本相同的字节码。
因此,我们几乎可以将“断言有多昂贵”的问题简化为“基于static final
条件的始终采用的分支跳过的代码有多昂贵?”。好消息是,如果方法被编译,这些分支通常会被 C2 编译器完全优化掉。当然,即使在这种情况下,您仍然需要支付一些费用:
第 (1) 点和 (2) 点是在运行时编译 (JIT) 期间而不是在 java-file-compile 时删除断言的直接结果。这是与 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 编译为无操作。
最后,值得注意的是,您可以根据需要获得类似 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
也就是说,断言没有留下任何痕迹。在这种情况下,我们可以真正说运行时成本为零。您可以通过使用包装变量的单个 StaticAssert 类来使其在整个项目中更可行SKIP_CHECKS
,并且您可以利用这个现有的assert
糖制作一个 1 行版本:
public int addHomebrew2(int x, int y) {
assert SKIP_CHECKS || (x > 0 && y > 0);
return x + y;
}
同样,这在 javac 时间编译为字节码,没有任何断言的痕迹。不过,您将不得不处理有关死代码的 IDE 警告(至少在 Eclipse 中)。
1我的意思是这个问题可能是零影响,然后对周围的代码进行一个小的无害更改后,它可能会突然产生很大的影响。基本上,由于“内联或不内联”决策的二元效应,各种惩罚级别被高度量化。
2至少对于在运行时编译/运行断言相关代码的最重要部分。当然,JVM 中有少量支持接受-ea
命令行参数和翻转默认断言状态(但如上所述,您可以通过属性以通用方式实现相同的效果)。
非常非常少。我相信它们在类加载过程中被删除。
我得到的最接近的证明是: Java 语言规范中的断言语句规范。它似乎是这样措辞的,以便可以在类加载时处理断言语句。
禁用断言完全消除了它们的性能损失。一旦禁用,它们在语义和性能上本质上等同于空语句