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

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

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

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

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

23 ответа

Решение

Современные компиляторы 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.

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

Результаты

| Операция | Время выполнения в наносекундах за операцию | Относительно instanceof |
| ------------ | -------------------------------------|------------------------|
| INSTANCEOF | 39,598 ± 0,022 нс / оп | 100,00 %               |
| GETCLASS   | 39 687 ± 0,021 нс / оп | 100,22 %               |
| ТИП | 46,295 ± 0,026 нс / оп | 116,91 %               |
| ОО | 48 078 ± 0,026 нс / оп | 121,42 %               |

ТЛ; др

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

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

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

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

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

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

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

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

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

InstanceOf 3156
class== 2925 
OO 3083 
Id 3067 

Отвечая на ваш последний вопрос: если только профайлер не скажет вам, что вы тратите смехотворное количество времени в экземпляре: да, вы придирчивы.

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

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

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

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

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

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

ш расширяет х

А реализует ш

Б расширяет А

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

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

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

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

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

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

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

Я пишу тест производительности на основе 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();
}
}

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

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

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

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

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

o.doEverything();

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

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

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

Это где шаблоны приходят...

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

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

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

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

if (o instanceof java.lang.String)

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

if (objectStruct->iAmInstanceOf == &java_lang_String_class)

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

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

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

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

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

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

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

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

Someobject.doSomething();

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

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

if(a instanceof AnyObject){
}

быстрее как:

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

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

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

Если ваша единственная цель - скорость, то использование 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;
}

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

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

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

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

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

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

seq = seq.head();

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

Я также предпочитаю подход 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 и любым другим методом проверки того же самого, вероятно, даже не поддается измерению. Если производительность критична, то Java, вероятно, не тот язык. Основная причина в том, что вы не можете контролировать, когда виртуальная машина решает, что она хочет собирать мусор, что может привести к увеличению загрузки ЦП до 100% в течение нескольких секунд в большой программе (MagicDraw 10 отлично подходит для этого). Если вы не контролируете каждый компьютер, на котором будет работать эта программа, вы не сможете гарантировать, на какой версии JVM она будет работать, и у многих из более старых были серьезные проблемы со скоростью. Если это маленькое приложение, вы можете быть в порядке с Java, но если вы постоянно читаете и отбрасываете данные, то вы заметите, когда включится GC.

Другие вопросы по тегам