Влияние производительности использования instanceof в Java


Answers

Подход

Я написал контрольную программу для оценки различных реализаций:

  1. instanceof реализации (как ссылка)
  2. объект, ориентированный через абстрактный класс, и @Override метод тестирования
  3. используя реализацию собственного типа
  4. getClass() == _.class

Я использовал jmh для запуска теста с 100 вызовами разминки, 1000 итераций под измерением и с 10 вилами. Таким образом, каждый вариант был измерен 10 000 раз, что занимает 12:18:57 для запуска всего теста на моем MacBook Pro с macOS 10.12.4 и Java 1.8. Контрольный показатель измеряет среднее время каждого варианта. Подробнее см. Мою реализацию в GitHub .

Ради полноты: есть предыдущая версия этого ответа и мой бенчмарк .

Результаты

| Operation  | Runtime in nanoseconds per operation | Relative to instanceof |
|------------|--------------------------------------|------------------------|
| INSTANCEOF | 39,598 ± 0,022 ns/op                 | 100,00 %               |
| GETCLASS   | 39,687 ± 0,021 ns/op                 | 100,22 %               |
| TYPE       | 46,295 ± 0,026 ns/op                 | 116,91 %               |
| OO         | 48,078 ± 0,026 ns/op                 | 121,42 %               |

ТЛ; др

В Java 1.8 instanceof является самым быстрым подходом, хотя getClass() очень близок.

Question

Я работаю над приложением, и один подход к дизайну включает чрезвычайно интенсивное использование оператора instanceof . Хотя я знаю, что дизайн OO обычно пытается избежать использования instanceof , это совсем другая история, и этот вопрос связан исключительно с производительностью. Мне было интересно, есть ли влияние на производительность? Это так же быстро, как == ?

Например, у меня есть базовый класс с 10 подклассами. В одной функции, которая принимает базовый класс, я проверяю, является ли класс экземпляром подкласса и выполняет некоторую процедуру.

Один из других способов, которые я решил решить, состоял в том, чтобы вместо этого использовать примитив типа «идентификатор типа», а также использовать битовую маску для представления категорий подклассов, а затем просто выполнить сравнение бит-маски подкласса «идентификатор типа» с постоянная маска, представляющая категорию.

Является ли instanceof каким-то образом оптимизированным JVM быстрее? Я хочу придерживаться Java, но производительность приложения имеет решающее значение. Было бы здорово, если бы кто-то, кто был на этом пути раньше, мог предложить некоторые советы. Я слишком сильно зацикляюсь или фокусируюсь на неправильной вещи для оптимизации?




В современной версии Java оператор instanceof быстрее, чем простой вызов метода. Это означает:

if(a instanceof AnyObject){
}

быстрее:

if(a.getType() == XYZ){
}

Другое дело, если вам нужно каскадировать много экземпляров. Тогда переключатель, который только вызывает getType (), быстрее.




Что касается примечания Питера Лоури о том, что вам не нужен экземпляр для окончательных классов и вы можете просто использовать ссылочное равенство, будьте осторожны! Несмотря на то, что последние классы не могут быть расширены, они не гарантированно загружаются одним и тем же загрузчиком классов. Используйте только x.getClass () == SomeFinal.class или его ilk, если вы абсолютно уверены, что в этом разделе кода есть только один загрузчик классов.




Трудно сказать, как определенный JVM реализует экземпляр, но в большинстве случаев объекты сопоставимы с структурами и классами, а также каждый объект struct имеет указатель на структуру класса, в которой он является экземпляром. Так что фактически instanceof для

if (o instanceof java.lang.String)

может быть так же быстро, как следующий код C

if (objectStruct->iAmInstanceOf == &java_lang_String_class)

предполагая, что компилятор JIT на месте и выполняет достойную работу.

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

Это не обязательно, однако, это сильно зависит от JVM. Однако, если это окажется причиной узкого места в вашем коде, я считаю, что реализация JVM довольно бедна. Даже тот, у которого нет JIT-компилятора и только интерпретирует код, должен иметь возможность сделать экземпляр теста практически без времени.




Я вернусь к вам в качестве примера. Но способ избежать проблемы (или ее отсутствия) в целом заключался бы в создании родительского интерфейса для всех подклассов, на которых вам нужно делать instanceof. Интерфейс будет супер-набором всех методов в подклассах, для которых вам нужно выполнить проверку экземпляра. Если метод не применяется к определенному подклассу, просто укажите фиктивную реализацию этого метода. Если бы я не понял это неправильно, я так и пережил проблему в прошлом.




«instanceof» на самом деле является оператором, например + или -, и я считаю, что он имеет свою собственную инструкцию по байт-коду JVM. Это должно быть очень быстро.

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




Демьян и Павел упоминают хороший момент; однако размещение кода для выполнения действительно зависит от того, как вы хотите использовать данные ...

Я большой поклонник небольших объектов данных, которые можно использовать разными способами. Если вы будете следовать методу переопределения (полиморфного), ваши объекты могут использоваться только «в одну сторону».

Вот где шаблоны ...

Вы можете использовать двойную отправку (как в шаблоне посетителя), чтобы просить каждый объект «вызывать» вас сам, это решит тип объекта. Однако (опять же) вам понадобится класс, который может «делать вещи» со всеми возможными подтипами.

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

Надеюсь, это вдохновит некоторые другие идеи ...

package com.javadude.sample;

import java.util.HashMap;
import java.util.Map;

public class StrategyExample {
    static class SomeCommonSuperType {}
    static class SubType1 extends SomeCommonSuperType {}
    static class SubType2 extends SomeCommonSuperType {}
    static class SubType3 extends SomeCommonSuperType {}

    static interface Handler<T extends SomeCommonSuperType> {
        Object handle(T object);
    }

    static class HandlerMap {
        private Map<Class<? extends SomeCommonSuperType>, Handler<? extends SomeCommonSuperType>> handlers_ =
            new HashMap<Class<? extends SomeCommonSuperType>, Handler<? extends SomeCommonSuperType>>();
        public <T extends SomeCommonSuperType> void add(Class<T> c, Handler<T> handler) {
            handlers_.put(c, handler);
        }
        @SuppressWarnings("unchecked")
        public <T extends SomeCommonSuperType> Object handle(T o) {
            return ((Handler<T>) handlers_.get(o.getClass())).handle(o);
        }
    }

    public static void main(String[] args) {
        HandlerMap handlerMap = new HandlerMap();

        handlerMap.add(SubType1.class, new Handler<SubType1>() {
            @Override public Object handle(SubType1 object) {
                System.out.println("Handling SubType1");
                return null;
            } });
        handlerMap.add(SubType2.class, new Handler<SubType2>() {
            @Override public Object handle(SubType2 object) {
                System.out.println("Handling SubType2");
                return null;
            } });
        handlerMap.add(SubType3.class, new Handler<SubType3>() {
            @Override public Object handle(SubType3 object) {
                System.out.println("Handling SubType3");
                return null;
            } });

        SubType1 subType1 = new SubType1();
        handlerMap.handle(subType1);
        SubType2 subType2 = new SubType2();
        handlerMap.handle(subType2);
        SubType3 subType3 = new SubType3();
        handlerMap.handle(subType3);
    }
}



Я подумал, что, может быть, стоит представить контрпример к общему консенсусу на этой странице, что «instanceof» недостаточно дорого, чтобы беспокоиться. Я обнаружил, что у меня есть код во внутреннем цикле, который (в некоторой исторической попытке оптимизации)

if (!(seq instanceof SingleItem)) {
  seq = seq.head();
}

где вызов head () на SingleItem возвращает значение без изменений. Замена кода на

seq = seq.head();

дает мне ускорение от 269 мс до 169 мс, несмотря на то, что в цикле происходят довольно тяжелые вещи, такие как преобразование строк в двойное. Конечно, возможно, что ускорение связано с устранением условной ветви, чем с устранением самого экземпляра самого оператора; но я думал, что стоит упомянуть.




У меня такой же вопрос, но из-за того, что я не нашел «метрики производительности» для случая использования, подобного моему, я сделал еще несколько примеров кода. На моем оборудовании и Java 6 и 7 разница между экземпляром и переключением на 10 миллионов итераций

for 10 child classes - instanceof: 1200ms vs switch: 470ms
for 5 child classes  - instanceof:  375ms vs switch: 204ms

Таким образом, instanceof действительно медленнее, особенно при огромном количестве операторов if-else-if, однако разница будет незначительной в реальном приложении.

import java.util.Date;

public class InstanceOfVsEnum {

    public static int c1, c2, c3, c4, c5, c6, c7, c8, c9, cA;

    public static class Handler {
        public enum Type { Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9, TypeA }
        protected Handler(Type type) { this.type = type; }
        public final Type type;

        public static void addHandlerInstanceOf(Handler h) {
            if( h instanceof H1) { c1++; }
            else if( h instanceof H2) { c2++; }
            else if( h instanceof H3) { c3++; }
            else if( h instanceof H4) { c4++; }
            else if( h instanceof H5) { c5++; }
            else if( h instanceof H6) { c6++; }
            else if( h instanceof H7) { c7++; }
            else if( h instanceof H8) { c8++; }
            else if( h instanceof H9) { c9++; }
            else if( h instanceof HA) { cA++; }
        }

        public static void addHandlerSwitch(Handler h) {
            switch( h.type ) {
                case Type1: c1++; break;
                case Type2: c2++; break;
                case Type3: c3++; break;
                case Type4: c4++; break;
                case Type5: c5++; break;
                case Type6: c6++; break;
                case Type7: c7++; break;
                case Type8: c8++; break;
                case Type9: c9++; break;
                case TypeA: cA++; break;
            }
        }
    }

    public static class H1 extends Handler { public H1() { super(Type.Type1); } }
    public static class H2 extends Handler { public H2() { super(Type.Type2); } }
    public static class H3 extends Handler { public H3() { super(Type.Type3); } }
    public static class H4 extends Handler { public H4() { super(Type.Type4); } }
    public static class H5 extends Handler { public H5() { super(Type.Type5); } }
    public static class H6 extends Handler { public H6() { super(Type.Type6); } }
    public static class H7 extends Handler { public H7() { super(Type.Type7); } }
    public static class H8 extends Handler { public H8() { super(Type.Type8); } }
    public static class H9 extends Handler { public H9() { super(Type.Type9); } }
    public static class HA extends Handler { public HA() { super(Type.TypeA); } }

    final static int cCycles = 10000000;

    public static void main(String[] args) {
        H1 h1 = new H1();
        H2 h2 = new H2();
        H3 h3 = new H3();
        H4 h4 = new H4();
        H5 h5 = new H5();
        H6 h6 = new H6();
        H7 h7 = new H7();
        H8 h8 = new H8();
        H9 h9 = new H9();
        HA hA = new HA();

        Date dtStart = new Date();
        for( int i = 0; i < cCycles; i++ ) {
            Handler.addHandlerInstanceOf(h1);
            Handler.addHandlerInstanceOf(h2);
            Handler.addHandlerInstanceOf(h3);
            Handler.addHandlerInstanceOf(h4);
            Handler.addHandlerInstanceOf(h5);
            Handler.addHandlerInstanceOf(h6);
            Handler.addHandlerInstanceOf(h7);
            Handler.addHandlerInstanceOf(h8);
            Handler.addHandlerInstanceOf(h9);
            Handler.addHandlerInstanceOf(hA);
        }
        System.out.println("Instance of - " + (new Date().getTime() - dtStart.getTime()));

        dtStart = new Date();
        for( int i = 0; i < cCycles; i++ ) {
            Handler.addHandlerSwitch(h1);
            Handler.addHandlerSwitch(h2);
            Handler.addHandlerSwitch(h3);
            Handler.addHandlerSwitch(h4);
            Handler.addHandlerSwitch(h5);
            Handler.addHandlerSwitch(h6);
            Handler.addHandlerSwitch(h7);
            Handler.addHandlerSwitch(h8);
            Handler.addHandlerSwitch(h9);
            Handler.addHandlerSwitch(hA);
        }
        System.out.println("Switch of - " + (new Date().getTime() - dtStart.getTime()));
    }
}



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

Почему это? Поскольку, вероятно, произойдет то, что у вас есть несколько интерфейсов, которые обеспечивают некоторую функциональность (скажем, интерфейсы x, y и z), а некоторые объекты для управления могут (или нет) реализовать один из этих интерфейсов ... но не напрямую. Скажем, например, у меня есть:

w расширяет x

A реализует w

B расширяет A

C расширяет B, реализует y

D расширяет C, реализует z

Предположим, что я обрабатываю экземпляр D, объект d. Computing (d instanceof x) требует взять d.getClass (), пропустить через интерфейсы, которые он реализует, чтобы знать, является ли один из них равным x, и если не сделать это снова рекурсивно для всех своих предков ... В нашем случае, если вы сделаете широкое исследование этого дерева, получим по крайней мере 8 сравнений, предположив, что y и z не расширяют ничего ...

Сложность дерева деривации реального мира, вероятно, будет выше. В некоторых случаях JIT может оптимизировать большую часть его, если она в состоянии разрешить заранее d как бы во всех возможных случаях экземпляр чего-либо, расширяющего x. Реально, однако, вы собираетесь пройти через этот обход дерева большую часть времени.

Если это станет проблемой, я бы предложил вместо этого использовать карту обработчика, связав конкретный класс объекта с закрытием, который выполняет обработку. Он удаляет фазу обхода дерева в пользу прямого сопоставления. Однако будьте осторожны, если вы установили обработчик для C.class, мой объект d выше не будет распознан.

вот мои 2 цента, я надеюсь, они помогут ...




Элементы, которые будут определять влияние производительности:

  1. Число возможных классов, для которых оператор instanceof мог бы возвращать true
  2. Распределение ваших данных - большая часть экземпляров операций, разрешенных в первой или второй попытке? Сначала вы захотите, чтобы вы вернули истинные операции.
  3. Среда развертывания. Работа на виртуальной машине Sun Solaris значительно отличается от Sun JVM от Sun. По умолчанию Solaris будет работать в режиме «сервер», а Windows будет работать в клиентском режиме. Оптимизация JIT для Solaris сделает доступ к всем методам одинаковым.

Я создал микрозапуск для четырех различных методов отправки . Результаты Solaris состоят в следующем: меньшее число выполняется быстрее:

InstanceOf 3156
class== 2925 
OO 3083 
Id 3067