Java Hotspot服务器中的多态性成本很高

当我在Java Hotspot客户端中运行我的计时测试程序时,我得到了一致的行为。 但是,当我在Hotspot服务器中运行它时,我得到了意想不到的结果。 从本质上讲,在我试图复制的某些情况下,多态性的成本是高得令人无法接受的。

这是Hotspot服务器的已知问题/错误,还是我做错了什么?

测试程序和时间如下:

Intel i7, Windows 8 Java HotSpot(TM) 64-Bit Server VM (build 24.45-b08, mixed mode) Mine2: 0.387028831 <--- polymorphic call with expected timing Trivial: 1.545411765 <--- some more polymorphic calls Mine: 0.727726371 <--- polymorphic call with unexpected timing. Should be about 0.38 Mine: 0.383132698 <--- direct call with expected timing 

随着我添加额外的测试,情况变得更糟。 列表末尾附近的测试时间完全关闭。

 interface canDoIsSquare { boolean isSquare(long x); } final class Trivial implements canDoIsSquare { @Override final public boolean isSquare(long x) { if (x > 0) { long t = (long) Math.sqrt(x); return t * t == x; } return x == 0; } @Override public String toString() {return "Trivial";} } final class Mine implements canDoIsSquare { @Override final public boolean isSquare(long x) { if (x > 0) { while ((x & 3) == 0) x >>= 2; if ((x & 2) != 0 || (x & 7) == 5) return false; final long t = (long) Math.sqrt(x); return (t * t == x); } return x == 0; } @Override public String toString() {return "Mine";} } final class Mine2 implements canDoIsSquare { @Override final public boolean isSquare(long x) { // just duplicated code for this test if (x > 0) { while ((x & 3) == 0) x >>= 2; if ((x & 2) != 0 || (x & 7) == 5) return false; final long t = (long) Math.sqrt(x); return (t * t == x); } return x == 0; } @Override final public String toString() {return "Mine2";} } public class IsSquared { static final long init = (long) (Integer.MAX_VALUE / 8) * (Integer.MAX_VALUE / 2) + 1L; static long test1(final canDoIsSquare fun) { long r = init; long startTimeNano = System.nanoTime(); while (!fun.isSquare(r)) ++r; long taskTimeNano = System.nanoTime() - startTimeNano; System.out.println(fun + ": " + taskTimeNano / 1e9); return r; } static public void main(String[] args) { Mine mine = new Mine(); Trivial trivial = new Trivial(); Mine2 mine2 = new Mine2(); test1(mine2); test1(trivial); test1(mine); long r = init; long startTimeNano = System.nanoTime(); while (!mine.isSquare(r)) ++r; long taskTimeNano = System.nanoTime() - startTimeNano; System.out.println(mine + ": " + taskTimeNano / 1e9); System.out.println(r); } } 

实际上,成本很高,但是你的基准并没有衡量真正相关的东西。 JIT可以优化大部分开销,但你没有给它任何机会。 见例如这里 。

无论如何,没有基准热身,而且还有On Stack Replacement 。

解释可能是Server Hotspot优化得更好但速度更慢。 它假设它有足够的时间并且收集更长的必要统计数据。 因此,当客户端热点优化您的程序时,服务器热点正在准备自己生成更好的代码。

附加测试恶化的原因是最初的单形呼叫站点变为双态,然后变形。

实际上,可能只调用其中一个方法。 如果您想要基准测试,则必须在自己的JVM中运行每个测试。 这是一个真正的痛苦,但现有的基准测试框架为您做到了。

或者您可能想要测量多态情况,但是您需要首先使用所有情况预热代码。 通过这种方式,即使在单个JVM中,您也可以找到哪种方法更快(尽管每个JVM都会因为变形调用开销而减慢速度。

更新

解释似乎是从单形到巨型的变化。 当第一个测试运行时,JVM知道所有类(因为实例已经创建),但乐观地认为只有Mine2出现在调用站点上。 所以它做了一个快速检查(翻译为条件分支,总是正确预测,因此非常快),并称为正确的方法。 由于后来看到其他两个实例在那里使用,它必须为它们创建一个分支表(分支预测仍然有效,但开销更高)。

有什么不清楚:JVM可以将这个测试移出循环,从而将其成本降低到几乎为零。 我不知道为什么它不会发生。

简而言之,JIT可以优化单个方法调用和两个方法调用,以多种多态调用的方式进行优化。 可能在任何给定行上调用的可能方法的数量是重要的,JIT会随着时间的推移建立这个图片。 当一个方法被内联时,可以进一步优化,但在你的情况下,有问题的行会增加test1在运行期间可能的方法调用次数,因此它会变慢。

我解决这个问题的方法是复制简短的测试代码,以便对每个类进行相同的测试(假设这是现实的)如果你的程序在运行时会是多态的,那么你应该测试它是否真实看它可以改变结果。

当您从一个新循环运行该方法时,您会看到仅从该行代码调用一个方法的好处。

以下是您可能会看到的不同成本的表格,具体取决于任何单个行可以调用的可能方法的数量。 http://vanillajava.blogspot.co.uk/2012/12/performance-of-inlined-virtual-method.html

多态性不是为了提高性能而设计的,对我来说,完全合理的是,随着多态性的复杂性增加,它应该更慢。

BTW制作方法final不再提高性能。 如果您逐行调用子类,则JIT可以解决(如上所述)


编辑正如您所看到的, client JVM并没有像设计那样优化代码,而是相对较轻的8个启动时间。 这意味着客户端JVM更加一致,但速度一直较慢。 如果您想获得最佳性能,则需要考虑一些优化策略,这些策略会根据是否应用优化来产生多种可能的结果。