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

Java 为什么接口方法调用比具体调用慢?,java,Java,当我发现抽象类和接口之间的区别时,我想到了这个问题。 在年,我开始知道接口很慢,因为它们需要额外的间接寻址。 但是我不知道接口需要什么类型的间接寻址,而不是抽象类或具体类。请澄清一下。 提前感谢对象具有某种类型的“vtable指针”,它指向其类的“vtable”(方法指针表)(“vtable”可能是错误的术语,但这并不重要)。vtable有指向所有方法实现的指针;每个方法都有一个对应于表项的索引。因此,要调用类方法,只需在vtable中查找相应的方法(使用其索引)。如果一个类扩展了另一个类,那么

当我发现抽象类和接口之间的区别时,我想到了这个问题。 在年,我开始知道接口很慢,因为它们需要额外的间接寻址。 但是我不知道接口需要什么类型的间接寻址,而不是抽象类或具体类。请澄清一下。 提前感谢

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

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


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

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

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;
        }
    }
}
公共类类测试{
公共静态void main(字符串[]args){
随机=新随机();
List foos=新阵列列表(40000);
列表栏=新的ArrayList(40000);
对于(int i=0;i<40000;i++){
添加(random.nextBoolean()?new Foo1Impl():new Foo2Impl());
添加(random.nextBoolean()?new Bar1Impl():new Bar2Impl());
}
长启动=System.nanoTime();
for(Foo-Foo:foos){
foo.foo();
}
System.out.println(System.nanoTime()-start);
start=System.nanoTime();
用于(条形:条形){
bar.bar();
}
System.out.println(System.nanoTime()-start);
}
抽象静态类Foo{
公共摘要int foo();
}
静态接口条{
int-bar();
}
静态类Foo1Impl扩展了Foo{
@凌驾
公共int foo(){
int i=10;
i++;
返回i;
}
}
静态类Foo2Impl扩展了Foo{
@凌驾
公共int foo(){
int i=10;
i++;
返回i;
}
}
静态类Bar1Impl实现Bar{
@凌驾
公共int-bar(){
int i=10;
i++;
返回i;
}
}
静态类Bar2Impl实现Bar{
@凌驾
公共int-bar(){
int i=10;
i++;
返回i;
}
}
}

有许多性能神话,有些可能在几年前是真的,有些可能在没有JIT的虚拟机上仍然是真的

Android文档(记住Android没有JVM,他们有Dalvik VM)曾经说过,在接口上调用方法要比在类上调用慢,所以他们有助于传播这个神话(也有可能是在开启JIT之前,Dalvik VM上的调用慢)。文件中现在确实指出:

性能神话

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

在没有JIT的设备上,通过 具有确切类型而不是接口的变量稍微多一些 有效率的(因此,例如,在服务器上调用方法更便宜。) HashMap比map映射更重要,即使在这两种情况下,map都是 HashMap。)这并不是说速度慢了2倍;实际的 差异更像是慢了6%。此外,JIT使这两个 实际上无法区分

资料来源:


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

这是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 interface method call was 4.2 ns
The average abstract method call was 4.1 ns
在运行测试的方式上,与您选择的测试相比,差异更大

我在Java6Update26和OpenJDK7中得到了相同的结果


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

The direct method call was 2.2 ns

我试图编写一个测试,量化调用方法的各种方式。我的发现表明,重要的不是方法是否是接口方法,而是调用它的引用类型。通过类引用调用接口方法要比通过接口引用调用同一类上的同一方法快得多(相对于调用次数而言)

1000000个呼叫的结果是

通过接口参考的接口方法:(纳米,毫)5172161.0,5.0

抽象参考界面方法:(Nano,millis)1893732.0,1.8

通过顶级衍生参考的接口方法:(纳米,毫)1841659.0,1.8

通过具体类的具体方法
The direct method call was 2.2 ns
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;
    }
}