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


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

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

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

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


Answers



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

ТЛ; др

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




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

в цикле 10.000.000 экземпляр Of дал мне 63-96 мс, а строка равнялась дам мне 106-230 мс

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

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

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




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

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

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

InstanceOf 3156
class== 2925 
OO 3083 
Id 3067 



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

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

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

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

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




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

По-видимому, если класс X не имеет подклассов (JVM знает), instanceof может быть оптимизирован как:

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

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

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

Хорошая новость!




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 цента, я надеюсь, они помогут ...




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

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




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

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




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




Трудно сказать, как определенный JVM реализует экземпляр, но в большинстве случаев объекты сопоставимы с структурами и классами, а также каждый объект struct имеет указатель на структуру класса, в которой он является экземпляром. Так что фактически 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();



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

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

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

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

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

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




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




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

Вы должны больше рассказать о проблеме, возможно, вы можете использовать ассоциативный магазин, например карту <класс, объект>, если вас интересуют только конкретные типы.




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




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