为什么接口方法调用比具体的调用慢?

当我发现抽象类和接口之间的区别时,这就是我想到的问题。 在这篇文章中,我知道接口很慢,因为它们需要额外的间接。 但是我没有得到接口要求的什么types的间接方法,也没有得到抽象类或具体类的请求。请澄清一下。 提前致谢

有许多performance神话,有些在几年前可能是真实的,有些可能在没有JIT的虚拟机上也是如此。

Android文档(记住,Android没有JVM,他们有Dalvik虚拟机)曾经说过,在一个接口上调用一个方法比在一个类上调用它慢,所以他们有助于传播神话(这也是可能的在打开JIT之前,它在Dalvik虚拟机上的速度比较慢)。 文档现在说:

表演神话

本文档的以前版本提出了各种误导性声明。 我们在这里解决一些问题。

在没有JIT的设备上,通过一个确切types的variables而不是一个接口来调用方法是比较有效的。 (所以,例如,调用HashMap地图上的方法比Map地图更便宜,即使在这两种情况下地图都是HashMap。)情况并非如此,速度慢了2倍; 实际的差异更像是慢了6%。 而且,JIT使这两者有效地区分开来。

来源: 在Android上devise性能

JVM中的JIT可能也是如此,否则将会非常奇怪。

如果有疑问,测量它。 我的结果显示没有显着差异。 运行时,生成以下程序:

7421714 (abstract) 5840702 (interface) 7621523 (abstract) 5929049 (interface) 

但是当我切换两个循环的地方:

 7887080 (interface) 5573605 (abstract) 7986213 (interface) 5609046 (abstract) 

看起来,抽象类的速度稍快(〜6%),但不应该引起注意。 这些是纳秒。 7887080纳秒是〜7毫秒。 这相当于每40k调用0.1毫秒(Java版本:1.6.20)

代码如下:

 public class ClassTest { public static void main(String[] args) { Random random = new Random(); List<Foo> foos = new ArrayList<Foo>(40000); List<Bar> bars = new ArrayList<Bar>(40000); for (int i = 0; i < 40000; i++) { foos.add(random.nextBoolean() ? new Foo1Impl() : new Foo2Impl()); bars.add(random.nextBoolean() ? new Bar1Impl() : new Bar2Impl()); } long start = System.nanoTime(); for (Foo foo : foos) { foo.foo(); } System.out.println(System.nanoTime() - start); start = System.nanoTime(); for (Bar bar : bars) { bar.bar(); } System.out.println(System.nanoTime() - start); } abstract static class Foo { public abstract int foo(); } static interface Bar { int bar(); } static class Foo1Impl extends Foo { @Override public int foo() { int i = 10; i++; return i; } } static class Foo2Impl extends Foo { @Override public int foo() { int i = 10; i++; return i; } } static class Bar1Impl implements Bar { @Override public int bar() { int i = 10; i++; return i; } } static class Bar2Impl implements Bar { @Override public int bar() { int i = 10; i++; return i; } } } 

一个对象具有某种types的“vtable指针”,指向其类的“vtable”(方法指针表)(“vtable”可能是错误的术语,但这不重要)。 vtable有指向所有方法实现的指针; 每种方法都有一个对应于表项的索引。 因此,要调用一个类方法,只需在vtable中查找相应的方法(使用它的索引)即可。 如果一个类扩展了另一个类,则它具有更长的vtable和更多的条目; 从基类调用一个方法仍然使用相同的过程:也就是说,通过它的索引来查找方法。

但是,在通过接口引用从接口调用方法时,必须有一些替代机制来查找方法实现指针。 因为一个类可以实现多个接口,所以方法不可能在vtable中始终具有相同的索引(例如)。 有各种可能的方法来解决这个问题,但没有办法像简单的vtable调度一样高效。

但是,正如评论中所提到的,它可能与现代Java虚拟机实现没有太大的区别。

这是Bozho例子的变化。 它运行时间更长,并重新使用相同的对象,所以caching大小无关紧要。 我也使用一个数组,所以没有从迭代器开销。

 public static void main(String[] args) { Random random = new Random(); int testLength = 200 * 1000 * 1000; Foo[] foos = new Foo[testLength]; Bar[] bars = new Bar[testLength]; Foo1Impl foo1 = new Foo1Impl(); Foo2Impl foo2 = new Foo2Impl(); Bar1Impl bar1 = new Bar1Impl(); Bar2Impl bar2 = new Bar2Impl(); for (int i = 0; i < testLength; i++) { boolean flip = random.nextBoolean(); foos[i] = flip ? foo1 : foo2; bars[i] = flip ? bar1 : bar2; } long start; start = System.nanoTime(); for (Foo foo : foos) { foo.foo(); } System.out.printf("The average abstract method call was %.1f ns%n", (double) (System.nanoTime() - start) / testLength); start = System.nanoTime(); for (Bar bar : bars) { bar.bar(); } System.out.printf("The average interface method call was %.1f ns%n", (double) (System.nanoTime() - start) / testLength); } 

版画

 The average abstract method call was 4.2 ns The average interface method call was 4.1 ns 

如果您交换命令testing运行您得到

 The average interface method call was 4.2 ns The average abstract method call was 4.1 ns 

你如何运行testing比select哪一个有更多的不同。

我得到了与Java 6 update 26和OpenJDK 7相同的结果。


顺便说一句:如果你添加一个循环只调用同一个对象,每一次,你会得到

 The direct method call was 2.2 ns 

我试图编写一个testing来量化所有可能被调用的方法。 我的研究结果表明,不是一种方法是否是一种接口方法,而是你所称的引用的types。 通过类引用调用接口方法比通过接口引用在同一类上调用同一方法要快得多(相对于调用次数)。

100万电话的结果是…

通过接口参考的接口方法:(nanos,millis)5172161.0,5.0

接口方法通过抽象引用:(nanos,millis)1893732.0,1.8

通过顶层派生参考的接口方法:(nanos,millis)1841659.0,1.8

具体方法通过具体的类参考:(nanos,millis)1822885.0,1.8

请注意,结果的前两行是调用完全相同的方法,但通过不同的引用。

这里是代码…

 package interfacetest; /** * * @author rpbarbat */ public class InterfaceTest { static public interface ITest { public int getFirstValue(); public int getSecondValue(); } static abstract public class ATest implements ITest { int first = 0; @Override public int getFirstValue() { return first++; } } static public class TestImpl extends ATest { int second = 0; @Override public int getSecondValue() { return second++; } } static public class Test { int value = 0; public int getConcreteValue() { return value++; } } static int loops = 1000000; /** * @param args the command line arguments */ public static void main(String[] args) { // Get some various pointers to the test classes // To Interface ITest iTest = new TestImpl(); // To abstract base ATest aTest = new TestImpl(); // To impl TestImpl testImpl = new TestImpl(); // To concrete Test test = new Test(); System.out.println("Method call timings - " + loops + " loops"); StopWatch stopWatch = new StopWatch(); // Call interface method via interface reference stopWatch.start(); for (int i = 0; i < loops; i++) { iTest.getFirstValue(); } stopWatch.stop(); System.out.println("interface method via interface reference: (nanos, millis)" + stopWatch.getElapsedNanos() + ", " + stopWatch.getElapsedMillis()); // Call interface method via abstract reference stopWatch.start(); for (int i = 0; i < loops; i++) { aTest.getFirstValue(); } stopWatch.stop(); System.out.println("interface method via abstract reference: (nanos, millis)" + stopWatch.getElapsedNanos() + ", " + stopWatch.getElapsedMillis()); // Call derived interface via derived reference stopWatch.start(); for (int i = 0; i < loops; i++) { testImpl.getSecondValue(); } stopWatch.stop(); System.out.println("interface via toplevel derived reference: (nanos, millis)" + stopWatch.getElapsedNanos() + ", " + stopWatch.getElapsedMillis()); // Call concrete method in concrete class stopWatch.start(); for (int i = 0; i < loops; i++) { test.getConcreteValue(); } stopWatch.stop(); System.out.println("Concrete method via concrete class reference: (nanos, millis)" + stopWatch.getElapsedNanos() + ", " + stopWatch.getElapsedMillis()); } } package interfacetest; /** * * @author rpbarbat */ public class StopWatch { private long start; private long stop; public StopWatch() { start = 0; stop = 0; } public void start() { stop = 0; start = System.nanoTime(); } public void stop() { stop = System.nanoTime(); } public float getElapsedNanos() { return (stop - start); } public float getElapsedMillis() { return (stop - start) / 1000; } public float getElapsedSeconds() { return (stop - start) / 1000000000; } } 

这是使用ORACLE JDK 1.6_24。 希望这有助于把这个问题睡觉…

问候,

罗德尼·巴尔巴蒂