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

StackOverflow https://stackoverflow.com/questions/103564

  •  01-07-2019
  •  | 
  •  

Вопрос

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

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

Один из других способов, которым я думал решить это, состоял в том, чтобы использовать вместо него целочисленный примитив "type id" и использовать битовую маску для представления категорий подклассов, а затем просто выполнить сравнение битовой маски подклассов "type id" с постоянной маской, представляющей категорию.

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

Это было полезно?

Решение

Современные компиляторы JVM / JIC устранили снижение производительности большинства традиционно "медленных" операций, включая instanceof, обработку исключений, отражение и т.д.

Как писал Дональд Кнут, "Мы должны забыть о небольшой эффективности, скажем, примерно в 97% случаев:преждевременная оптимизация - корень всего зла ". Производительность instanceof, вероятно, не будет проблемой, поэтому не тратьте свое время на поиск экзотических обходных путей, пока не будете уверены, что проблема именно в этом.

Другие советы

Подход

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

  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 %               |

tl;dr

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

Я только что провел простой тест, чтобы увидеть, как производительность instanceOf сравнивается с простым вызовом s.equals() для объекта string, состоящего только из одной буквы.

в цикле 10.000.000 instanceOf дал мне 63-96 мс, а строка equals дала мне 106-230 мс

Я использовал Java jvm 6.

Таким образом, в моем простом тесте быстрее выполнить instanceOf вместо сравнения строки из одного символа.

использование Integer's .equals() вместо string's дало мне тот же результат, только когда я использовал == я был быстрее instanceOf на 20 мс (в цикле 10.000.000)

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

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

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

InstanceOf 3156
class== 2925 
OO 3083 
Id 3067 

Отвечаю на ваш самый последний вопрос:Если только профилировщик не скажет вам, что вы тратите смехотворное количество времени на instanceof:Да, ты придираешься.

Прежде чем задуматься об оптимизации чего-то, что никогда не нуждалось в оптимизации:Напишите свой алгоритм наиболее читаемым способом и запустите его.Запускайте его до тех пор, пока jit-компилятор не получит возможность оптимизировать его сам.Если затем у вас возникнут проблемы с этим фрагментом кода, используйте профилировщик, который подскажет вам, где можно получить максимальную отдачу и оптимизировать это.

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

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

Я забыл:Никогда не измеряйте первый заход.

У меня такой же вопрос, но поскольку я не нашел "показателей производительности" для варианта использования, подобного моему, я сделал еще несколько примеров кода.На моем оборудовании и Java 6 и 7 разница между instanceof и switch на 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 работает действительно быстро, требуя всего нескольких инструкций процессора.

Очевидно, что если класс X не имеет загруженных подклассов (JVM знает), instanceof может быть оптимизирован следующим образом:

     x instanceof X    
==>  x.getClass()==X.class  
==>  x.classID == constant_X_ID

Основная стоимость - это просто чтение!

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

Всем хороших новостей!

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

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

w расширяет x

A орудия w

B расширяет A

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

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

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

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

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

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

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

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

Instanceof работает очень быстро.Это сводится к байт-коду, который используется для сравнения ссылок на классы.Попробуйте несколько миллионов экземпляров в цикле и убедитесь в этом сами.

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

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

Вот тут-то и появляются паттерны...

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

Я предпочитаю использовать шаблон стратегии, где вы можете регистрировать стратегии для каждого подтипа, с которым вы хотите работать.Что-то вроде следующего.Обратите внимание, что это помогает только при точном совпадении типов, но имеет то преимущество, что оно расширяемо - сторонние участники могут добавлять свои собственные типы и обработчики.(Это хорошо для динамических фреймворков, таких как 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 очень эффективен, так что ваша производительность вряд ли пострадает.Однако использование большого количества instanceof предполагает проблему с дизайном.

Если вы можете использовать xClass == String.class, это будет быстрее.Примечание:вам не нужен instanceof для конечных классов.

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

if (o instanceof java.lang.String)

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

if (objectStruct->iAmInstanceOf == &java_lang_String_class)

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

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

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

Экземпляр это предупреждение о плохом объектно-ориентированном дизайне.

Текущие JVM действительно означают Экземпляр само по себе это не сильно беспокоит производительность.Если вы обнаруживаете, что часто используете его, особенно для основной функциональности, вероятно, пришло время взглянуть на дизайн.Выигрыш в производительности (и простоте / ремонтопригодности) от рефакторинга с целью улучшения дизайна значительно перевесит любые фактические циклы процессора, затраченные на фактический Экземпляр позвони.

Приведу очень маленький упрощенный пример программирования.

if (SomeObject instanceOf Integer) {
  [do something]
}
if (SomeObject instanceOf Double) {
  [do something different]
}

Плохая архитектура лучшим выбором было бы, чтобы SomeObject был родительским классом двух дочерних классов, где каждый дочерний класс переопределяет метод (doSomething), чтобы код выглядел как таковой:

Someobject.doSomething();

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

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

if (o instanceof Class1)
   doThis();
else if (o instanceof Class2)
   doThat();
//...

Вы можете заменить это на

o.doEverything();

и затем есть реализация "doEverything()", в Class1 вызывающая "doThis()", а в Class2 вызывающая "doThat()", и так далее.

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

if(a instanceof AnyObject){
}

быстрее, так как:

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

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

Если скорость - ваша единственная цель, то использование констант int для идентификации подклассов, похоже, сокращает миллисекунду времени

static final int ID_A = 0;
static final int ID_B = 1;
abstract class Base {
  final int id;
  Base(int i) { id = i; }
}
class A extends Base {
 A() { super(ID_A); }
}
class B extends Base {
 B() { super(ID_B); }
}
...
Base obj = ...
switch(obj.id) {
case  ID_A: .... break;
case  ID_B: .... break;
}

ужасный OO-дизайн, но если ваш анализ производительности показывает, что именно здесь ваше узкое место, то, возможно.В моем коде код отправки занимает 10% от общего времени выполнения, и это, возможно, способствовало улучшению общей скорости на 1%.

Я пишу тест производительности на основе jmh-java-benchmark-archetype: 2.21.JDK - это openjdk, а версия - 1.8.0_212.Тестовая машина - Mac pro.Результатом теста является:

Benchmark                Mode  Cnt    Score   Error   Units
MyBenchmark.getClasses  thrpt   30  510.818 ± 4.190  ops/us
MyBenchmark.instanceOf  thrpt   30  503.826 ± 5.546  ops/us

Результат показывает, что:getClass лучше, чем instanceOf, что противоречит другим тестам.Однако я не знаю почему.

Тестовый код приведен ниже:

public class MyBenchmark {

public static final Object a = new LinkedHashMap<String, String>();

@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public boolean instanceOf() {
    return a instanceof Map;
}

@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public boolean getClasses() {
    return a.getClass() == HashMap.class;
}

public static void main(String[] args) throws RunnerException {
    Options opt =
        new OptionsBuilder().include(MyBenchmark.class.getSimpleName()).warmupIterations(20).measurementIterations(30).forks(1).build();
    new Runner(opt).run();
}
}

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

Вам следует подробнее рассказать о проблеме, возможно, вы могли бы использовать ассоциативное хранилище, напримеркарта<Class, Object=""> если вас интересуют только конкретные типы.

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

Я также предпочитаю подход enum, но я бы использовал абстрактный базовый класс, чтобы заставить подклассы реализовывать getType() способ.

public abstract class Base
{
  protected enum TYPE
  {
    DERIVED_A, DERIVED_B
  }

  public abstract TYPE getType();

  class DerivedA extends Base
  {
    @Override
    public TYPE getType()
    {
      return TYPE.DERIVED_A;
    }
  }

  class DerivedB extends Base
  {
    @Override
    public TYPE getType()
    {
      return TYPE.DERIVED_B;
    }
  }
}

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

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

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

seq = seq.head();

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

Ты сосредотачиваешься не на том.Разница между instanceof и любым другим методом проверки одного и того же, вероятно, даже не поддается измерению.Если производительность имеет решающее значение, то Java, вероятно, не тот язык.Основная причина заключается в том, что вы не можете контролировать, когда виртуальная машина решит, что хочет пойти собирать мусор, что может привести к разгону процессора до 100% на несколько секунд в большой программе (MagicDraw 10 отлично подходил для этого).Если вы не контролируете каждый компьютер, на котором будет запущена эта программа, вы не можете гарантировать, на какой версии JVM она будет установлена, а у многих старых были серьезные проблемы со скоростью.Если это небольшое приложение, возможно, у вас все в порядке с Java, но если вы постоянно читаете и удаляете данные, то вы будет обратите внимание, когда запускается GC.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top