如何编写(测试)代码,使编译器/JIT不进行优化?

11

我对编译器和JIT优化的内部机制并不了解,但通常我会尝试使用“常识”来猜测哪些可以被优化,哪些不能。所以今天我写了一个简单的单元测试方法:

@Test  // [Test] in C#
public void testDefaultConstructor() {
    new MyObject();
}

这个方法实际上是我需要的。它检查默认构造函数是否存在并且能够正常运行,但是我开始考虑编译器/JIT优化的影响。编译器/JIT是否可以通过完全消除new MyObject();语句来优化此方法?当然,它需要确定调用图没有对其他对象产生副作用的情况,这是普通构造函数的典型情况,它只是初始化对象的内部状态。

我认为只有JIT能够执行这种优化。这可能意味着我不必担心,因为测试方法只执行一次。我的假设正确吗?

尽管如此,我仍在思考这个一般性的主题。当我考虑如何防止此方法被优化时,我想我可以 assertTrue(new MyObject().toString() != null),但这非常依赖于toString()方法的实际实现,即使如此,JIT也可能确定toString() 方法始终返回非空字符串(例如,如果实际上调用了Object.toString()),从而优化整个分支。所以这种方式行不通。

我知道在C#中,可以使用[MethodImpl(MethodImplOptions.NoOptimization)],但这不是我真正寻找的。我希望找到一种(与语言无关的)方法,以确保我的代码的某些特定部分将按照我期望的那样运行,而不会被JIT干扰这个过程。

此外,在创建单元测试时,还有哪些典型的优化情况应该注意?

非常感谢!

7个回答

5

我认为,如果你担心它会被优化掉,可能意味着你正在进行过度测试。

在静态语言中,我倾向于将编译器视为一种测试。如果通过了编译,那么就意味着某些东西存在(比如方法)。如果你没有另一个测试来测试默认构造函数(这将证明它不会抛出异常),你可能需要考虑为什么首先要编写该默认构造函数(YAGNI等等)。

我知道有人不同意我的看法,但我觉得这种东西只会因无用的原因而增加你的测试数量,即使从TDD角度看也是如此。


我同意你的看法。在阅读了你的回答后,我认为如果这个测试更重要,不仅仅是一个“编译”测试,那么肯定会有其他测试使用默认构造函数。感谢您的答复! - Hosam Aly
在像Java这样的语言中,编译和执行环境可能会有很大的差异。如果正在构建的类与测试代码分开存放,那么进行这样的测试可能是有意义的。特别有趣的是,使用自定义类加载器从某种形式的动态存储中实时加载类 - 这段代码将确保查找实际上是有效的。 - Sam Harwell

5
不用担心,永远不允许优化任何可能对系统产生影响的东西(除了速度)。如果你新建一个对象,会调用代码,分配内存,必须保证它能工作。
如果你使用if(false)来保护它,其中false是一个final常量,那么它就可以被完全优化出系统,然后检测到该方法不起作用并将其优化掉(理论上)。
另外,它还可以聪明地确定这个方法:
newIfTrue(boolean b) {
    if(b)
        new ThisClass();
}

如果 b 为假,则始终不执行操作,并最终发现在代码的某个点 B 总是为假,从而完全将此例程编译出该代码。

这就是 JIT 能够完成的几乎不可能在任何非托管语言中实现的东西。


谢谢。我想知道JIT为什么会这样做?如果一个对象分配是无用的(在某些情况下可以通过静态分析确定),为什么JIT不会进行优化呢? - Hosam Aly
我现在可以想到一个边角情况,但我认为这种情况很少见。例如,如果对象分配是为了确保某些其他对象有足够的内存可用(甚至确保不会发生分页),那么优化将使假设无效。 - Hosam Aly
需要将Java虚拟机(JVM)保持在一致的状态,以便程序代码按照Java内存模型在JVM中执行。如果JIT可以证明代码对可观察的程序状态没有影响,则不需要实际执行任何特定代码或分配内存。 - Sam Harwell
是的,它必须像执行代码一样准确地运行,因此如果它经过优化而执行任何不同的内容,则担心毫无意义... - Bill K

2
JIT 只能执行不影响语言保证语义的操作。理论上,如果 JIT 能够保证调用没有副作用并且永远不会抛出异常(不包括 OutOfMemoryError),则可以删除对 MyObject 构造函数的分配和调用。
换句话说,如果 JIT 优化掉了你测试中的调用,那么你的测试仍然会通过
注:这适用于你正在进行的是功能测试而非性能测试。在性能测试中,确保 JIT 不会优化你正在测量的操作很重要,否则你的结果将变得无用。

对于这个话题有很好的见解。还有一个补充:当使用性能测试来测试功能时(例如,尝试确定CPU缓存大小),我们应该明确添加 [MethodImpl(MethodImplOptions.NoOptimization)] 以避免JIT做出一些技巧。 - M. Mimpen

2

换个角度看:

假设编译器能够确定调用图没有任何副作用(我认为这是不可能的,模糊地记得我的计算机科学课上有关P = NP的内容)。它将优化没有副作用的任何方法。 由于大多数测试都没有并且不应该有任何副作用,因此编译器可以将所有这些测试都优化掉。


1

看起来在C#中我可以这样做:

[Test]
public void testDefaultConstructor() {
    GC.KeepAlive(new MyObject());
}

据我所知,GC.KeepAlive方法不会被JIT内联,因此代码将保证按预期工作。但是,我不知道Java中是否有类似的结构。


1
-1:具有误导性。这并不是需要使用GC.KeepAlive(甚至根本没有任何好处)的情况。 - Sam Harwell

1
每个I/O操作都是一个副作用,所以你可以直接放置。
Object obj = new MyObject();
System.out.println(obj.toString());

而且你没问题。


是的,这确实是一种方法。但单元测试通常不应该有输出语句。 - Hosam Aly
我认为只要你不依赖于I/O来确定测试是否通过,那么就没问题。 - quant_dev
1
-1:误导性的。没有I/O他也能做得很好。这个答案让人觉得这种形式的单元测试需要I/O才能保证正确性。 - Sam Harwell

0

为什么这很重要呢?如果编译器/即时编译器可以静态确定不会遇到任何断言(可能会导致副作用),那么你就没问题了。


还需要验证构造函数是否存在,不会影响静态程序状态,并且不能抛出 JVM 隐式要求的异常,例如 NullPointerException - Sam Harwell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接