asi*_*iar 27 java performance assertions
代码可以使用其中的断言进行编译,并且可以在需要时激活/取消激活.
但是,如果我部署一个带有断言的应用程序并且那些被禁用,那么在那里被忽略的惩罚是什么?
Bee*_*ope 38
与传统观点相反,断言确实会产生运行时影响并可能影响性能.在大多数情况下,这种影响可能很小,但在某些情况下可能很大.断言在运行时降低速度的一些机制相当"平滑"且可预测(通常很小),但下面讨论的最后一种方法(内联失败)很棘手,因为它是最大的潜在问题(你可能有一个数量级回归)并且它不平滑1.
在分析assertJava中的功能时,一个好处是它们在字节码/ JVM级别上并不是什么神奇的东西.也就是说,它们是.class在(.java文件)编译时使用标准Java机制在文件中实现的,并且它们没有得到JVM 2的任何特殊处理,而是依赖于适用于任何运行时编译代码的常规优化.
让我们快速浏览一下究竟他们是如何在一个现代化的Oracle JDK 8实施(但据我所知还没有在几乎永远改变).
使用单个断言采用以下方法:
public int addAssert(int x, int y) {
assert x > 0 && y > 0;
return x + y;
}
Run Code Online (Sandbox Code Playgroud)
...编译该方法并使用以下代码反编译字节码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
Run Code Online (Sandbox Code Playgroud)
字节码的前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
Run Code Online (Sandbox Code Playgroud)
因此,编译器已创建此隐藏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;
}
Run Code Online (Sandbox Code Playgroud)
这里我们只是简单地写出断言正在做什么(我们甚至可以组合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
Run Code Online (Sandbox Code Playgroud)
嗯,它与字节版本的字节相同.
因此,我们几乎可以将"断言有多昂贵"的问题减少为"基于static final条件的总是采用分支跳过代码的代价是多少?".好消息是,如果编译该方法,这些分支通常会被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;
}
Run Code Online (Sandbox Code Playgroud)
这编译为:
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
Run Code Online (Sandbox Code Playgroud)
...因此将该函数的大小从26个减少到9个字节,其中5个与断言相关.当然,丢失的字节码刚刚转移到另一个函数,但这很好,因为它会在内联决策中被单独考虑,而当断言被禁用时,JIT编译为无操作.
最后,值得注意的是,如果需要,可以获得C/C++ - 就像编译时断言一样.这些是断言,其开/关状态被静态编译为二进制(在javac时间).如果要启用断言,则需要新的二进制文件.另一方面,这种类型的断言在运行时是真正自由的.
如果我们将自制SKIP_CHECKS更改为static final在编译时知道,如下所示:
public static final boolean SKIP_CHECKS = true;
Run Code Online (Sandbox Code Playgroud)
然后addHomebrew汇编到:
public int addHomebrew(int, int);
Code:
0: iload_1
1: iload_2
2: iadd
3: ireturn
Run Code Online (Sandbox Code Playgroud)
也就是说,断言没有任何痕迹.在这种情况下,我们可以真正地说运行成本为零.通过使用包含SKIP_CHECKS变量的单个StaticAssert类,您可以在整个项目中使其更加可行,并且您可以利用此现有assert糖来生成1行版本:
public int addHomebrew2(int x, int y) {
assert SKIP_CHECKS || (x > 0 && y > 0);
return x + y;
}
Run Code Online (Sandbox Code Playgroud)
同样,这在javac时间编译为字节码而没有断言的痕迹.您将不得不处理关于死代码的IDE警告(至少在eclipse中).
1这个,我的意思是这个问题可能没有效果,然后在对周围代码进行一次无害的改变后,它可能会突然产生很大的影响.基本上,由于"内联或不内联"决策的二元效应,各种惩罚级别被大量量化.
2至少在运行时编译/运行与断言相关的代码的最重要部分.当然,JVM中有少量支持接受-ea命令行参数并翻转默认断言状态(但如上所述,您可以通过属性以通用方式实现相同的效果).
| 归档时间: |
|
| 查看次数: |
3102 次 |
| 最近记录: |