Подтвердить что ты не робот

Почему вызовы метода интерфейса медленнее, чем конкретные вызовы?

Это вопрос возникает, когда я нахожу разницу между абстрактным классом и интерфейсом. В этот пост Я узнал, что интерфейсы медленны, поскольку они требуют дополнительной косвенности. Но я не понимаю, какой тип косвенности требуется интерфейсу, а не абстрактному классу или конкретному классу. Пожалуйста, уточните его. Спасибо заранее

4b9b3361

Ответ 1

Есть много мифов о производительности, и некоторые из них, вероятно, были верны несколько лет назад, и некоторые из них могут быть верны для виртуальных машин, у которых нет JIT.

Документация на Android (помните, что Android не имеет JVM, у них есть Dalvik VM), говорилось, что вызов метода на интерфейсах был медленнее, чем вызов его в классе, поэтому они способствовали распространению мифа ( также возможно, что он был медленнее на Дальвике, пока они не включили JIT). В документации теперь говорится:

Мифы о производительности

Предыдущие версии этого документа делали различные вводящие в заблуждение утверждения. Мы обратитесь к некоторым из них здесь.

На устройствах без JIT это правда, что вызов методов через переменная с точным типом, а не с интерфейсом, немного больше эффективный. (Так, например, было дешевле ссылаться на методы на HashMap, чем карта карты, хотя в обоих случаях карта была HashMap.) Это было не так, это было в два раза медленнее; Настоящий разница была больше похожа на 6% медленнее. Кроме того, JIT делает два фактически неразличимы.

Источник: Разработка для производительности на Android

То же самое, вероятно, верно для JIT в JVM, в противном случае было бы очень странно.

Ответ 2

Если есть сомнения, измерьте его. Мои результаты не показали существенной разницы. При запуске выдается следующая программа:

7421714 (abstract)
5840702 (interface)

7621523 (abstract)
5929049 (interface)

Но когда я переключил места двух циклов:

7887080 (interface)
5573605 (abstract)

7986213 (interface)
5609046 (abstract)

Похоже, что абстрактные классы немного (~ 6%) быстрее, но это не должно быть заметно; Это наносекунды. 7887080 наносекунд - ~ 7 миллисекунд. Это делает его разницей в 0,1 миллиса за 40 тыс. Вызовов (версия 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;
        }
    }
}

Ответ 3

У объекта есть "указатель vtable", который указывает на "vtable" (таблица указателей метода) для своего класса ( "vtable" может быть неправильной терминологией, но это не важно). Vtable имеет указатели на все реализации методов; каждый метод имеет индекс, который соответствует записи в таблице. Таким образом, для вызова метода класса вы просто просматриваете соответствующий метод (используя его индекс) в таблице vtable. Если один класс расширяет другой, у него просто больше vtable с большим количеством записей; вызов метода из базового класса по-прежнему использует ту же процедуру: то есть поиск метода по его индексу.

Однако при вызове метода из интерфейса через ссылку на интерфейс должен быть некоторый альтернативный механизм для поиска указателя реализации метода. Поскольку класс может реализовывать несколько интерфейсов, невозможно, чтобы метод всегда имел один и тот же индекс в таблице vtable (например). Существуют различные возможные способы решения этой проблемы, но это не так эффективно, как простая отправка vtable.

Однако, как упоминалось в комментариях, это, вероятно, не будет иметь большого значения с современной реализацией Java VM.

Ответ 4

Это вариация на примере Божо. Он работает дольше и повторно использует те же объекты, поэтому размер кеша не имеет большого значения. Я также использую массив, поэтому на итераторе нет накладных расходов.

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

Ответ 5

Я попытался написать тест, который бы количественно определял все способы, с помощью которых можно было бы вызвать методы. Мои результаты показывают, что это не тот метод, который метод интерфейса или не имеет значения, а скорее тип ссылки, через которую вы его вызываете. Вызов метода интерфейса с помощью ссылки на класс намного быстрее (относительно количества вызовов), чем вызов одного и того же метода в одном классе через ссылку на интерфейс.

Результаты для 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. Надеюсь, это поможет поставить этот вопрос в постель...

Привет,

Родни Барбати