【问题标题】:Will an iterator surrounding an assert statement affect performance of production build?围绕断言语句的迭代器会影响生产构建的性能吗?
【发布时间】:2014-01-06 22:48:27
【问题描述】:

我最近在 Java 中发现了“assert”语句,并且在我调试软件时一直在乱扔我的软件。我最初的直觉是避免仅仅为了处理断言语句而制作控制流语句,但后来我意识到这些控制语句可能会在生产构建期间被删除,因为它们的块是空的。我的印象是它们会被 JIT 编译器消除。

不幸的是,我模糊地回忆了 JIT 编译器的工作原理,并且找不到合适的文档。我能找到的最好的是 IBM 优化过程的 brief outline

在我养成实施基于断言的测试的习惯之前,我想知道是否有最佳实践来最大程度地减少它们对性能的影响。我不想执行一堆测试,却发现它们的集体影响是大大降低性能,即使它们的单独影响可以忽略不计。

谁能告诉我以下几行是否会拖累生产版本的性能(默认情况下禁用“断言”)?

for (T obj : Collection){
    assert obj.someProperty();
}

另外,如果我是更复杂的东西,包括仅用于断言的短期对象,该怎么办?

TreeMap<Integer,T> map = new TreeMap<>();
int i = 0;
for (T obj : Collection){
    map.put(i,obj);
    assert obj.someProperty();
    i++;
}
// assert something about map, then never use it again

或者是唯一作用是调用“assert”的方法?

提前致谢!


Oracle 文档中有关“assert”声明的相关摘录:

这些讨论了如何从类文件中删除断言,这并不是我所关心的。

从类文件程序员中删除所有断言跟踪 为资源受限的设备开发应用程序可能希望 完全从类文件中删除断言。虽然这使它 不可能在现场启用断言,它也减少了类 文件大小,可能会提高类加载性能。在 缺乏高质量的 JIT,可能会导致 占用空间并提高运行时性能。

断言工具不直接支持剥离 类文件中的断言。然而,断言语句可以是 与描述的“条件编译”成语一起使用 在 Java 语言规范中,使编译器能够消除 来自它生成的类文件的所有这些断言的痕迹:

静态最终布尔断言 = ... ; // false 以消除断言

如果(断言)断言;

在常见问题解答部分:

为什么不提供一个编译器标志来完全消除断言 从目标文件?这是一个坚定的要求,它是可能的 在现场启用断言,以增强可维护性。它会 还可以允许开发人员消除断言 在编译时来自目标文件。断言可以包含边 效果,尽管它们不应该,因此这样的标志可能会改变 程序在重要方面的行为。它被认为是好的 每个有效的 Java 只有一个语义相关联的事情 程序。此外,我们希望鼓励用户将断言留在对象中 文件,以便它们可以在现场启用。最后,规范要求 当一个类在它之前运行时,断言的行为就像启用一样 初始化。提供这些语义是不可能的,如果 断言从类文件中被剥离。但请注意, Java 中描述的标准“条件编译习语” 语言规范可用于实现此效果 真正想要它的开发人员。

【问题讨论】:

  • 我不记得足够的细节来给出一个自信的答案,但我认为断言启用/禁用是在运行时,而不是在编译时。
  • @Brandon:感谢您的提醒。如果是这样的话,我想答案将是微不足道的。也许我需要阅读一些关于“断言”的内容。我正在添加问题的链接...

标签: java performance compiler-construction


【解决方案1】:

您可以从assert 中进行方法调用,因此我更喜欢的语法类似于:

private static boolean testSomePropertyTrue(Collection<T> collection) {
    boolean test = true;
    for (T obj : collection){
        test = test && obj.someProperty();
    }
    return test;
}

...

assert testSomePropertyTrue(collection);

这对于由 JIT 优化的代码没有歧义,并且在启用断言时将执行相同的不变检查。

正如所写,您的第二个示例将始终创建TreeMap,无论是否启用了断言。将整个事物包装在一个函数中并将其作为单个断言进行评估将在运行时完全消除该代码路径。

就像这个问题的另一个答案所暗示的那样,无论是否启用断言,它们都会留下字节码,但使用上面显示的样式将确定性地阻止这些代码路径执行,除非启用断言。

【讨论】:

  • 谢谢。当你这样说的时候,它似乎很明显。正如你所说,我注意到我首先收集信息然后用断言语句测试它的测试有些拖累。一种情况似乎很棘手——我想确认更新过程产生了预期的变化。这需要在进行更新之前保存以前的状态,这实际上是资源密集型步骤。我没有看到任何方法可以在更新数据的同时将测试提取到单独的方法中。
【解决方案2】:

谁能告诉我以下几行是否会拖累生产版本的性能(默认情况下禁用“断言”)?

无论断言是打开还是关闭,您的断言循环都将运行(即,它们是字节码的一部分——无论是在运行代码时启用断言)。但是,我不会担心第一个 sn-p 的性能; JIT 可能会意识到循环没有做任何事情并尽早处理它。

另外,如果我是更复杂的东西,包括仅用于断言的短期对象,该怎么办?

像你的第二个 sn-p 这样的事情更难确定 - 最好的办法可能是自己计时,尽管在大多数情况下我认为它也不会产生明显的影响。

也许你可以有一个“调试”标志并像这样使用它:

static boolean debug = true;

if (debug) {
    // loop here
}

【讨论】:

  • 您能否澄清您所说的循环“将运行”但 JIT 将“处理它”的意思?这听起来很矛盾。附言。我对 JIT 系统只有最模糊的了解,所以我可能会混淆一些术语。如果我继续使用 Java,这是我需要审查的事情之一。
  • @adam.r 当然:​​我的意思是即使关闭了断言,循环也将存在于字节码级别(即,如果您使用javac -p 查看字节码,那么您将看到循环)(事实上,断言是在运行代码时启用的,而不是编译),所以它本质上等同于只有一个空主体的循环。在这种情况下,当循环被执行时,JIT 足够聪明,可以检测到循环中没有发生任何事情,并将其优化掉。
  • 你的第二个循环不是那么简单,这就是为什么我建议最好自己计时,如果这是一个真正的问题。
  • 谢谢。我显然将字节码编译和机器码编译(JIT,运行时)混为一谈。我担心的不是单个测试会消耗过多的资源,而是许多测试的总和将是一个重要的资源消耗(并且难以识别)。所以我希望在养成这样设计测试的习惯之前大致了解这将如何影响我的程序。
  • 根据循环中涉及的迭代器的复杂性,确定循环所需的分析都是死代码可能超出当前的JIT,所以我不会依赖它被淘汰。至少使用 -XX:+PrintCompilation 以确保它实际上已在您在生产中使用的 JIT 中被消除。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-15
  • 2017-06-12
  • 2017-01-04
  • 2011-04-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多