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毫秒。这使得Java版本1.6.20中每40k次调用的差异为0.1毫秒

代码如下:

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

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

Android文档记得Android没有JVM,他们有Dalvik VM,他们常说在接口上调用方法要比在类上调用慢,所以他们有助于散布谣言,也有可能在他们打开JIT之前,Dalvik VM上的调用慢。文件中现在确实指出:

性能神话

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

在没有JIT的设备上,通过 具有确切类型而不是接口的变量稍微多一些 有效率的因此,例如,在一个服务器上调用方法更便宜 HashMap比map映射更重要,即使在这两种情况下,map都是 哈希映射。这并不是说速度慢了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个呼叫的结果是

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

通过抽象参考的接口方法:nanos,millis 1893732.0,1.8

通过顶级衍生参考的接口方法:Nano,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实现的。希望这有助于解决这个问题

问候,


Rodney Barbati

接口比抽象类慢,因为方法调用的运行时决策只会增加很少的时间代价

然而,随着JIT的出现,它将处理相同方法的重复调用,因此您可能会看到执行 仅在第一次调用时出现延迟,这也是非常小的


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

是什么源告诉您接口很慢?@Mat Log4J文档中指出,在Log4J中,日志记录请求是向Logger类的实例发出的。Logger是一个类,而不是一个接口。这在一定程度上降低了方法调用的成本,但牺牲了一些灵活性。对我来说,这意味着,在需要非常积极的优化时,接口可能是一个障碍。我不知道这有多准确,也从来没有出现过这种情况,我无法证明它的准确性,但它是一个信誉良好的来源。相关:。如果能看到JIT后显示出可测量差异的测试,我会非常感兴趣。Log4J已经存在了很长一段时间,可以相信12年前在1.4.2热点出现之前是真的。很难相信在现代JVM上进行JIT后会有任何可测量的差异。@Sanjay:你的第一篇文章是关于gcj的,这是一个糟糕的Java实现,他们做了必要的工作,但在纯生产质量方面,从来都不是很好。你的第二个链接只是把它当作一个事实来陈述,而没有给出任何关于他是如何得出这个结论的暗示。他可能只是在一篇10年的文章中读到的。我想知道JVMs是否使用C++常用的技术来支持多重继承。多个vtables,指针发出砰砰声etc@seand,我不这么认为,因为垃圾收集。如果可以确保指针始终指向对象的开头,那么实现GC就容易得多。然而,这并非不可能。当然,thunking也会有性能损失。这很可能发生在JVM impl代码的深处,而不是GC的代码中。JIT等可能正在动态构建vtable。我没有试着阅读代码,但在我看来这是一个合理的方法。你没有领会我的意思。接口引用是指向某个对象的指针。在Java中,它很可能指向对象本身。在C++中,基类指针可以指向派生类对象的中间。@ DeWouReDelySiSe如果头与字段固定偏移,则等于相同的东西。因此,接口的速度是直接的两倍。这是在JIT编译之后吗?@Kevin Kostlan:也许你不应该忽略点之前的数字。4.2和4.1的比率不是2…
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;
    }
}