在Java中使用instanceof的性能影响

The performance impact of using instanceof in Java

我正在开发一个应用程序,其中一种设计方法涉及到大量使用instanceof运算符。虽然我知道OO设计通常试图避免使用instanceof,但情况不同,这个问题纯粹与性能有关。我想知道是否有性能影响?是否和==一样快?

例如,我有一个带有10个子类的基类。在接受基类的单个函数中,我会检查类是否是子类的实例,并执行一些例程。

我认为解决这个问题的另一种方法是使用"type id"整数原语,使用位掩码表示子类的类别,然后将子类"type id"与表示类别的常量掩码进行位掩码比较。

jvm是否以某种方式优化了instanceof,使其速度更快?我想坚持Java,但应用程序的性能是至关重要的。如果以前在这条路上的人能提供一些建议,那就太酷了。我是吹毛求疵太多,还是专注于错误的事情来优化?


现代的JVM/JIC编译器已经消除了大多数传统的"慢"操作的性能冲击,包括instanceof、异常处理、反射等。

正如Donald Knuth所写,"我们应该忘记小效率,比如说97%的时间:过早的优化是万恶之源。"InstanceOf的性能可能不会成为一个问题,所以在确定这是问题所在之前,不要浪费时间来制定奇特的解决方案。


途径

我编写了一个基准程序来评估不同的实现:

  • instanceof实施(参考)
  • 通过抽象类和@Override测试方法定向的对象
  • 使用自己的类型实现
  • getClass() == _.class实施
  • 我使用JMH运行基准测试,有100个预热调用、1000个正在测量的迭代和10个分叉。因此,每个选项用10次000次测量,用12:18:57来运行MacBook Pro上的整个基准,使用MacOS 102.4和Java 1.8。基准衡量每个选项的平均时间。有关更多详细信息,请参阅我在GitHub上的实现。

    为了完整性:这个答案和我的基准有以前的版本。

    结果

    1
    2
    3
    4
    5
    6
    | 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 %               |

    DR

    在Java 1.8中,EDOCX1×1是最快的方法,尽管EDCOX1与5的距离非常接近。


    我刚刚做了一个简单的测试,看看InstanceOfPerformance与对只有一个字母的字符串对象的简单s.Equals()调用的比较情况。

    在一个10000.000循环中,instanceof给了我63-96ms,字符串equals给了我106-230ms。

    我使用Java JVM 6。

    因此,在我的简单测试中,执行instanceof而不是一个字符串比较更快。

    使用integer的.equals()而不是string的,得到了相同的结果,只有当我使用==i比instanceof快20毫秒时(在10000.000循环中)


    决定性能影响的项目有:

  • InstanceOf运算符可以返回true的可能类的数目
  • 您的数据分布-在第一次或第二次尝试中是否解决了大多数操作实例?你会想把最有可能返回真实操作放在首位。
  • 部署环境。在Sun Solaris虚拟机上运行与Sun的Windows JVM明显不同。默认情况下,Solaris将以"服务器"模式运行,而Windows将以客户端模式运行。在Solaris上的JIT优化将使所有方法都可以访问。
  • 我为四种不同的分派方法创建了一个微基准。Solaris的结果如下,较小的数字更快:

    1
    2
    3
    4
    InstanceOf 3156
    class== 2925
    OO 3083
    Id 3067


    回答你最后一个问题:除非一个剖析者告诉你,你花了大量的时间在一个例子中:是的,你在吹毛求疵。

    在考虑优化一些不需要优化的东西之前:用最易读的方式编写算法并运行它。运行它,直到JIT编译器有机会自行优化它。如果您在这段代码上有问题,可以使用一个分析器来告诉您,在哪里可以获得最大的收益并对其进行优化。

    在高度优化编译器的时候,您对瓶颈的猜测可能是完全错误的。

    在这个答案的真实精神中(我完全相信):一旦JIT编译器有机会优化它,我绝对不知道instanceof和==是如何关联的。

    我忘记了:永远不要测量第一次跑步。


    我也有同样的问题,但是因为我没有找到与我的用例类似的"性能指标",所以我做了更多的示例代码。在我的硬件和Java 6和7上,Stand和Ston 10Mn迭代之间的区别是

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

    因此,instanceof的速度确实较慢,特别是在大量if-else if语句上,但是在实际应用程序中,差异可以忽略不计。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    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非常快,只需要几个CPU指令。

    显然,如果类X没有加载子类(jvm知道),那么instanceof可以优化为:

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

    主要成本只是一个阅读!

    如果X确实加载了子类,则需要更多的读取;它们可能位于同一位置,因此额外的成本也非常低。

    大家好消息!


    InstanceOf在大多数现实世界的实现中可能比简单的等价物更昂贵(也就是说,InstanceOf是真正需要的,并且您不能通过覆盖一个常见方法来解决它,就像每本初学者教科书和上面的Demian建议的那样)。

    为什么会这样?因为可能会发生的事情是,您有几个接口,它们提供一些功能(比如,接口X、Y和Z),以及一些要操作的对象,这些对象可能(或不)实现这些接口中的一个…但不是直接的。比如说,我有:

    W扩展X

    工具W

    B扩展了

    C扩展B,实现Y

    D扩展C,实现Z

    假设我正在处理一个d的实例,对象d.computing(d instance of x)需要取d.getClass(),通过它实现的接口循环,以知道一个是否是==to x,如果不是,则对所有的祖先都递归执行此操作…在我们的例子中,如果您对这棵树进行宽度优先的探索,那么至少会产生8个比较,假设y和z没有扩展任何内容…

    现实世界中派生树的复杂性可能更高。在某些情况下,如果JIT能够预先解析d,那么它可以优化大部分的数据,因为在所有可能的情况下,它都是扩展x的一个实例。然而,实际上,大多数情况下,您将遍历该树。

    如果这成为一个问题,我建议使用处理程序映射,将对象的具体类链接到执行处理的闭包。它删除了树遍历阶段,有利于直接映射。但是,要注意,如果您为c.class设置了处理程序,上面的对象d将无法识别。

    这是我的2美分,我希望他们能帮上忙…


    Instanceof非常有效,因此您的性能不太可能受到影响。然而,使用大量实例表明了一个设计问题。

    如果可以使用xclass==string.class,则速度更快。注意:最终类不需要instanceof。


    Instanceof非常快。它归结为用于类引用比较的字节码。在一个循环中尝试几百万个实例,并亲自查看。


    "instanceof"实际上是一个运算符,如+或-,我相信它有自己的jvm字节码指令。它应该足够快。

    如果您有一个开关,在这里测试对象是否是某个子类的实例,那么您的设计可能需要重新编写。考虑将子类特定的行为下推到子类本身中。


    德米安和保罗提到了一个很好的观点;但是,要执行的代码的位置实际上取决于您想要如何使用数据…

    我是小数据对象的忠实粉丝,这些小数据对象可以在很多方面使用。如果遵循覆盖(多态)方法,则对象只能"单向"使用。

    这就是模式出现的地方…

    您可以使用双重分派(在访问者模式中)要求每个对象通过自身"调用您"——这将解析对象的类型。但是(再次)您需要一个类,它可以"处理"所有可能的子类型。

    我更喜欢使用策略模式,您可以在其中为要处理的每个子类型注册策略。像下面这样。请注意,这只对精确的类型匹配有帮助,但具有可扩展性的优势——第三方贡献者可以添加自己的类型和处理程序。(这对于OSGi这样的动态框架很好,可以在其中添加新的捆绑包)

    希望这能激发一些其他的想法…

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    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 check。如果方法不适用于特定的子类,只需提供此方法的虚拟实现。如果我没有误解这个问题,这就是我过去解决这个问题的方式。


    InstanceOf是面向对象设计不佳的警告。

    当前的JVM确实意味着InstanceOf本身并不太担心性能问题。如果你发现自己经常使用它,特别是核心功能,那么现在可能是时候看一下设计了。重构为更好的设计所带来的性能(以及简单性/可维护性)收益将大大超过实际instanceof调用所花费的任何实际处理器周期。

    给出一个非常小的简单编程示例。

    1
    2
    3
    4
    5
    6
    if (SomeObject instanceOf Integer) {
      [do something]
    }
    if (SomeObject instanceOf Double) {
      [do something different]
    }

    如果体系结构不好,最好是让某个对象成为两个子类的父类,其中每个子类重写一个方法(doSomething),因此代码看起来是这样的:

    1
    Someobject.doSomething();


    很难说某个JVM是如何实现的实例,但在大多数情况下,对象可以与结构相比较,类也可以,并且每个对象结构都有指向它所属的类结构的指针。所以实际上是

    1
    if (o instanceof java.lang.String)

    可能和下面的C代码一样快

    1
    if (objectStruct->iAmInstanceOf == &java_lang_String_class)

    假设有一个JIT编译器,并且做得很好。

    考虑到这仅仅是访问一个指针,在指针指向的某个偏移量处获取一个指针,并将其与另一个指针进行比较(这基本上与测试32位数字相等时的情况相同),我想说操作实际上可以非常快。

    不过,它不必,这很大程度上取决于JVM。但是,如果这会成为代码中的瓶颈操作,我会认为JVM实现相当差。即使是一个没有JIT编译器并且只解释代码的人,也应该能够在几乎没有时间的情况下生成一个测试实例。


    在现代Java版本中,作为一个简单的方法调用,操作符的速度更快。这意味着:

    1
    2
    if(a instanceof AnyObject){
    }

    更快为:

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

    另一件事是,如果需要级联多个实例。然后,只调用一次getType()的开关更快。


    一般来说,"instanceof"运算符在这种情况下不受欢迎的原因(InstanceOf正在检查这个基类的子类)是因为您应该将操作移入一个方法并为适当的子类重写它。例如,如果您有:

    1
    2
    3
    4
    5
    if (o instanceof Class1)
       doThis();
    else if (o instanceof Class2)
       doThat();
    //...

    你可以换成

    1
    o.doEverything();

    然后在类1调用"dothis()"和类2调用"dothat()"中实现"doeverything()",依此类推。


    如果速度是您的唯一目标,那么使用int常量来标识子类似乎需要几毫秒的时间。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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%。


    关于Peter Lawrey的注释,您不需要InstanceOf作为最终类,只需使用引用相等,请小心!即使最后的类不能被扩展,它们也不能保证由同一个类加载器加载。如果您绝对肯定该代码段中只有一个类加载器在运行,则仅使用x.getClass()==somefinal.class或其ilk。


    我认为这可能值得提交一个反例,在这个页面上的普遍共识,"instanceof"并不昂贵,不值得担心。我发现我在一个内部循环中有一些代码(在一些历史性的优化尝试中)做到了

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

    其中,对单个项调用head()将返回未更改的值。将代码替换为

    1
    seq = seq.head();

    尽管循环中发生了一些非常严重的事情,比如字符串到双精度转换,但我还是可以从269ms加速到169ms。当然,加速可能更多地是由于消除了条件分支,而不是由于消除了操作符本身的实例;但我认为值得一提。


    我也喜欢枚举方法,但我会使用抽象基类来强制子类实现getType()方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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;
        }
      }
    }

    如果这真的是项目中的性能问题,您应该测量/分析它。如果可能的话,我建议重新设计。我敢肯定,您不能打败平台的本机实现(用C语言编写)。在这种情况下,您还应该考虑多重继承。

    如果您只对具体类型感兴趣,可以使用关联存储,例如map


    你把注意力集中在错误的事情上。InstanceOf和任何其他检查同一事物的方法之间的差异可能都无法测量。如果性能很关键,那么Java可能是错误的语言。主要原因是你不能控制当虚拟机决定它要去收集垃圾时,它可以在一个大程序中把CPU带到100%,持续几秒钟(magicDraw10非常适合这样做)。除非你控制着每台运行这个程序的计算机,否则你不能保证它将在哪个版本的JVM上运行,而且许多老版本的JVM都有严重的速度问题。如果它是一个小的应用程序,你可能对Java有好处,但是如果你不断地阅读和丢弃数据,那么你会注意到GC何时进入。