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

时间:2011-07-27 05:52:16

标签: java

当我发现抽象类和接口之间的区别时,这是一个问题。 在this post我发现接口很慢,因为它们需要额外的间接。 但是我没有得到接口所需的什么类型的间接,而不是抽象类或具体类。请澄清它。 提前致谢

6 个答案:

答案 0 :(得分:38)

有许多性能神话,有些可能在几年前就已经存在,有些可能仍然适用于没有JIT的虚拟机。

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

  

表现神话

     

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

     

在没有JIT的设备上,通过a调用方法是正确的   具有确切类型而不是接口的变量稍微多一些   高效。 (例如,调用方法的成本更低   HashMap映射比一个Map映射,即使在这两种情况下映射都是一个   HashMap。)情况并非如此慢2倍;实际上   差异更像是慢了6%。此外,JIT制作了两个   实际上难以区分。

来源:Designing for performance on Android

对于JVM中的JIT,情况可能同样如此,否则会非常奇怪。

答案 1 :(得分:24)

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

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;
        }
    }
}

答案 2 :(得分:6)

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

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

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

答案 3 :(得分:6)

这是Bozho示例的变体。它运行时间更长并重新使用相同的对象,因此缓存大小并不重要。我也使用数组,因此迭代器没有开销。

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

如果你交换测试的顺序,你得到

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

您运行测试的方式与您选择的方式有所不同。

我在Java 6更新26和OpenJDK 7中获得了相同的结果。


BTW:如果你添加一个每次只调用同一个对象的循环,你就得到了

The direct method call was 2.2 ns

答案 4 :(得分:1)

我尝试编写一个测试,可以量化所有可能调用方法的方法。我的研究结果表明,方法是否是一种重要的接口方法,而不是您调用它的引用类型。通过类引用调用接口方法(相对于调用的数量)要比通过接口引用在同一个类上调用相同的方法快得多。

1,000,000个电话的结果是......

通过接口参考的接口方法:(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;
    }
}

这是使用Oracles JDK 1.6_24。希望这有助于把这个问题放到一边......

此致

罗德尼巴尔巴蒂

答案 5 :(得分:0)

接口比抽象类要慢,因为方法调用的运行时决策几乎不会增加时间负担,

然而,随着JIT的出现,它将处理相同方法的重复调用,因此您可能只会在第一次调用时看到性能滞后,这也非常小,

现在,对于Java 8,他们通过添加默认和静态函数,几乎使抽象类变得无用,