The performance impact of using instanceof in Java
我正在开发一个应用程序,其中一种设计方法涉及到大量使用
例如,我有一个带有10个子类的基类。在接受基类的单个函数中,我会检查类是否是子类的实例,并执行一些例程。
我认为解决这个问题的另一种方法是使用"type id"整数原语,使用位掩码表示子类的类别,然后将子类"type id"与表示类别的常量掩码进行位掩码比较。
jvm是否以某种方式优化了
现代的JVM/JIC编译器已经消除了大多数传统的"慢"操作的性能冲击,包括instanceof、异常处理、反射等。
正如Donald Knuth所写,"我们应该忘记小效率,比如说97%的时间:过早的优化是万恶之源。"InstanceOf的性能可能不会成为一个问题,所以在确定这是问题所在之前,不要浪费时间来制定奇特的解决方案。
途径
我编写了一个基准程序来评估不同的实现:
我使用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循环中)
决定性能影响的项目有:
我为四种不同的分派方法创建了一个微基准。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())); } } |
显然,如果类
1 2 3 | x instanceof X ==> x.getClass()==X.class ==> x.classID == constant_X_ID |
主要成本只是一个阅读!
如果
大家好消息!
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 |
如果体系结构不好,最好是让某个对象成为两个子类的父类,其中每个子类重写一个方法(doSomething),因此代码看起来是这样的:
1 | Someobject.doSomething(); |
很难说某个JVM是如何实现的实例,但在大多数情况下,对象可以与结构相比较,类也可以,并且每个对象结构都有指向它所属的类结构的指针。所以实际上是
1 |
可能和下面的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。当然,加速可能更多地是由于消除了条件分支,而不是由于消除了操作符本身的实例;但我认为值得一提。
我也喜欢枚举方法,但我会使用抽象基类来强制子类实现
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何时进入。