代码可以使用其中的断言进行编译,并且可以是activated/deactivated when needed。
但是如果我在其中部署一个带有断言的应用程序并且那些被禁用,那么在那里被忽略的惩罚会被忽略吗?
答案 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)和(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编译成无操作。
最后,值得注意的是,如果需要,可以获得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)
禁用断言会消除它们 性能惩罚完全。一旦 残疾人,他们本质上 相当于空语句 语义和性能