Java最终性能/优化

4

我正在对不同的数据结构进行基准测试时注意到,当我将变量声明为 final 时,性能提高了10-20%。

这真的让我很惊讶。我原本以为 final 关键字仅用于限制变量更改,并且优化会确定某个变量是否具有恒定值。

以下是示例:

import javafx.scene.input.KeyCode;
import java.util.*;

public class Main {
    static /*final*/ int LOOPS = Integer.MAX_VALUE / 100;

    static /*final*/ KeyCode[] keyCodes = KeyCode.values();

    public static void main(String[] args) {
        long startTime;
        long endTime;

        testEnumSet(); //warmup
        startTime = System.nanoTime();
        testEnumSet();
        endTime = System.nanoTime();
        System.out.println("  EnumSet: " + (endTime - startTime) + "ns");
    }

    static /*final*/ EnumSet<KeyCode> enumSet = EnumSet.noneOf(KeyCode.class);
    static void testEnumSet() {
        for (int i = 0; i < LOOPS; i++) {
            /*final*/ KeyCode add = getRandomKeyCode();
            if(!enumSet.contains(add)) enumSet.add(add);

            /*final*/ KeyCode remove = getRandomKeyCode();
            if(enumSet.contains(remove)) enumSet.remove(remove);
        }
    }

    /*final*/ static Random random = new Random();
    static KeyCode getRandomKeyCode() {
        return keyCodes[random.nextInt(keyCodes.length)];
    }
}

使用final关键字的代码:EnumSet: 652 266 207ns
不使用final关键字的代码:EnumSet: 802 121 596ns

这个差别一直都是可以重现的!为什么使用final关键字的代码执行速度如此之快,而没有使用final关键字的代码执行速度如此缓慢?为什么不会进行优化?另外,final关键字到底有何作用,以及在生成的字节码中有何不同呢?


Duplicate - user177800
1
@JarrodRoberson,最高评分的答案说没有性能提升,但我的基准测试显示有约20%的性能提升,因此我认为这不是重复。 - Jhonny007
最高评分并不总是意味着正确,有时只是意味着大多数人都错了。就像你的发现并不总是可重复的,而“最终”的结果也不总是能带来任何可衡量的收益。 - user177800
2
你的代码中使用了static final字段。将其转换为final实例字段(只需在main中创建一个实例),效果可能会消失,因为静态字段参与常量折叠。然而,实例字段不是常量(从实例到实例变化),因此仅从冗余加载消除中受益,这仅依赖于非易失性而不是最终性。因此,如果您想对不可变对象进行基准测试,而不仅仅是全局常量,则您的基准测试无用。 - the8472
是的,你说得对,尽管那不是我的基准测试的目标,只是我在旁边发现的一些东西,让我感到好奇。 - Jhonny007
最值得注意的是,当LOOPS被声明为final时,它将成为一个编译时常量,这意味着它的值会被内联。换句话说,对于这个变量,添加final修饰符实际上会改变使用它的代码。另一方面,对于局部变量,它的影响最小(甚至没有影响),因为类中不存在这些变量是否被声明为final的信息。因此,final的效果可能会有很大的差异。 - Holger
1个回答

4
如果有一些东西永远不会改变,你可以进行各种优化,如内联实际值而不是反复查找它。这只是你可以做的一件容易解释且最有益的事情之一。
还有许多其他更为深奥的事情发生,它们的影响要小得多。
如果你查看字节码,尤其是在JIT启动后,就会看到这一点。
将整个类设为“final”可能会产生类似的效果。
尽管如此,“final”引用并不能总是提供可衡量的收益,这取决于引用的使用方式。在这种情况下,如果你查看源代码,你会发现EnumSet在幕后做了很多特殊处理。不可变引用可能作为其中的一部分而被内联。
另请注意,你所看到的行为可能会在JVM的未来版本中消失或者在其他JVM实现中不存在。任何事情都可能在你身下发生变化,所以不要依赖于任何一个特定的实现。
这里有更多详细信息关于所有“final”习惯用法的。链接

是的,但我认为JVM足够聪明,如果一个变量从未被修改(特别是在如此简单的代码中),它就可以检测到它基本上是final的,并且能够进行优化。 - Jhonny007
就像我所说的,如果您无法在编译时检查引用值是否更改,则字段的final关键字将无法起作用,编译器也无法阻止您进行编译。或者我有什么遗漏吗? - Jhonny007
你无法预测未来,因为可能会有代码在编译时不可用而改变了领域。调用已编译代码的代码是无法预测的。就像我说的,这个问题太宽泛了,老实说我一开始就不应该回答它。 - user177800
1
HotSpot擅长优化非“final”变量,但是此示例在全局可见的“Random”实例上调用了一个“synchronized”方法,这意味着代码必须重新检查其他线程可能进行的中间修改。对于“KeyCode[] keyCodes”变量,它意味着再次执行边界检查,这就是为什么对于这个变量,无论是否为“final”,影响最大的原因。然后,有“int LOOPS = Integer.MAX_VALUE / 100”,当被声明为“final”时,它是一个编译时常量。对于所有其他变量,影响相对较小。 - Holger
如果原始字段不是私有的,则为true,但对于局部变量或私有字段,您的观点似乎无关紧要。正如Jhonny007所说,代码检查器和编译器都可以检测到是否写入了最终字段,以及该字段是否可以被降低访问级别,因此似乎合理的是编译器可以选择自动优化将局部变量和私有字段标记为final,并且默认情况下应该具有这种行为[特别是对于发布版本]。我假设现代Java编译器就是这样工作的。 :/ - swooby
显示剩余5条评论

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