在Java中使用instanceof的性能影响
我正在开发一个应用程序,一种设计方法涉及到对instanceinsof运算符的大量使用。虽然我知道面向对象设计通常会尝试避免使用" instanceof",但这是另一回事,这个问题与性能完全相关。我想知道是否会对性能产生影响?和==
一样快吗?
例如,我有一个包含10个子类的基类。在采用基类的单个函数中,我检查该类是否为子类的实例并执行一些例程。
我想解决的另一种方法是改用"类型id"整数基元,并使用位掩码表示子类的类别,然后将子类"类型id"与位掩码进行位掩码比较。代表类别的常量掩码。
JVM是否以某种方式优化了" instanceof"以使其更快?我想坚持使用Java,但是应用程序的性能至关重要。如果以前曾走过这条路的人可以提供一些建议,那将很酷。我是否挑剔或者专注于错误的东西进行优化?
解决方案
现代JVM / JIC编译器已消除了大多数传统的"慢速"操作对性能的影响,其中包括instanceof,异常处理,反射等。
正如Donald Knuth所写:"我们应该忘记效率低下的问题,大约有97%的时间是这样:过早的优化是万恶之源。" instanceof的性能可能不会成为问题,因此在确定这是问题之前,请不要浪费时间来提出奇异的解决方法。
通常,在这种情况下(其中instanceof正在检查此基类的子类)," instanceof"运算符不被接受的原因是,因为我们应该做的是将操作移至方法中并覆盖相应的方法子类。例如,如果我们有:
if (o instanceof Class1) doThis(); else if (o instanceof Class2) doThat(); //...
我们可以将其替换为
o.doEverything();
然后在Class1中调用" doThis()",并在Class2中调用" doThat()",以实现" doEverything()",依此类推。
" instanceof"实际上是一个运算符,例如+或者-,我相信它具有自己的JVM字节码指令。它应该足够快。
我不应该说,如果有一个开关在测试对象是否是某个子类的实例,那么可能需要重新设计。考虑将特定于子类的行为推入子类本身。
Instanceof非常快。它归结为用于类引用比较的字节码。循环尝试数百万个instanceofs,自己看看。
如果这确实是项目中的性能问题,则应进行测量/配置。如果可以的话,我建议我们重新设计。我很确定我们不会击败平台的本机实现(用C编写)。在这种情况下,我们还应该考虑多重继承。
我们应该详细说明该问题,也许我们可以使用关联商店,例如如果我们仅对具体类型感兴趣,则为Map <Class,Object>。
回答最后一个问题:除非探查员告诉我们,否则我们在以下情况下花费了荒谬的时间:是的,我们在挑剔。
在想知道不需要优化的东西之前,请:以最易读的方式编写算法并运行它。运行它,直到jit编译器有机会对其自身进行优化。如果我们对这段代码有疑问,请使用探查器来告诉我们,在哪里获得最大收益并对其进行优化。
在高度优化的编译器时代,我们对瓶颈的猜测可能完全是错误的。
按照这个答案的真实精神(我全心全意地相信):一旦jit编译器有机会对其进行优化,我绝对不知道instanceof和==的关系。
我忘记了:永远不要衡量第一轮。
我们正在专注于错误的事情。 instanceof和检查同一事物的任何其他方法之间的差异可能甚至无法测量。如果性能至关重要,那么Java可能是错误的语言。主要原因是我们无法控制VM何时决定要收集垃圾,这可能会使大型程序中的CPU几秒钟达到100%的运行速度(MagicDraw 10非常适合这样做)。除非我们控制着每台计算机,否则将无法运行该程序,因此无法保证它将运行在哪个版本的JVM上,并且许多较旧的版本都存在严重的速度问题。如果它是一个小型应用程序,那么我们可以使用Java,但是如果我们不断读取和丢弃数据,那么我们会注意到GC启动的时间。
很难说某个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();
在现代Java版本中,instanceof运算符作为一个简单的方法调用速度更快。这表示:
if(a instanceof AnyObject){ }
更快,因为:
if(a.getType() == XYZ){ }
另一件事是,如果我们需要级联许多instanceof。然后,仅调用一次getType()的开关会更快。
德米安和保罗提到了一个要点。但是,执行代码的位置实际上取决于我们要如何使用数据...
我非常喜欢可以以多种方式使用的小数据对象。如果遵循覆盖(多态)方法,则只能"单向"使用对象。
这是模式出现的地方...
我们可以使用两次调度(如访问者模式)来要求每个对象传递自身的"呼叫信息",这将解决该对象的类型。但是(再次),我们将需要一个可以对所有可能的子类型"做事"的类。
我更喜欢使用策略模式,我们可以在其中为要处理的每个子类型注册策略。类似于以下内容。请注意,这仅有助于精确的类型匹配,但具有的优点是它的可扩展第三方贡献者可以添加自己的类型和处理程序。 (这对于像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性能与仅对一个字母的字符串对象进行简单的s.equals()调用相比。
在10.000.000循环中,instanceOf给了我63-96ms,而字符串equals给了我106-230ms
我使用了Java jvm 6.
因此,在我的简单测试中,执行instanceOf而不是比较一个字符串会更快。
仅当使用== i比instanceOf快20ms(在10.000.000循环中)时,使用Integer的.equals()而不是string的结果相同。
在大多数现实世界的实现中,instanceof可能会比简单的等价物昂贵(也就是说,确实需要instanceof的实现,而我们不仅可以通过覆盖通用方法来解决它,例如每本初学者的教材以及上面的Demian建议)。
这是为什么?因为可能要发生的事情是我们有几个接口,它们提供了一些功能(例如,接口x,y和z),以及一些要操作的对象,这些对象可能(也可能不)实现这些接口之一...但是不直接。举例来说,我有:
w延伸x
一个工具w
B延伸A
C扩展B,实现y
D扩展C,实现z
假设我正在处理D的一个实例,即对象d。计算(x instanceof x)需要采用d.getClass(),在其实现的接口中循环以了解x是否==,如果不是,则对所有祖先再次递归。
在我们的情况下,如果我们对那棵树进行广度优先探索,那么至少会进行8次比较,假设y和z不会扩展任何内容...
实际派生树的复杂性可能会更高。在某些情况下,如果JIT能够提前将d解析为在所有可能的情况下都是扩展x的实例,则JIT可以对其进行优化。实际上,大多数情况下,我们将经历该树的遍历。
如果这成为问题,我建议改用处理程序映射,将对象的具体类链接到进行处理的闭包。它消除了树遍历阶段,从而支持直接映射。但是,请注意,如果我们为C.class设置了处理程序,则上述我的对象d将不会被识别。
这是我的2美分,希望他们能帮忙...
instanceof非常高效,因此性能不太可能受到影响。
但是,使用大量instanceof会提出一个设计问题。
如果可以使用xClass == String.class,则速度更快。注意:最终课程不需要instanceof。
关于彼得·劳瑞(Peter Lawrey)的注释,我们不需要为最终类使用instanceof,而只需使用引用相等,请当心!即使不能扩展最终的类,也不能保证它们可以由相同的类加载器加载。如果我们绝对肯定该代码段仅在使用一个类加载器,则仅使用x.getClass()== SomeFinal.class或者其同类。
决定性能影响的项目包括:
- instanceof运算符可以返回true的可能类的数量
- 数据分布-在第一次或者第二次尝试中是否解决了大多数instanceof操作?我们将想把最有可能返回真实操作的位置放在首位。
- 部署环境。在Sun Solaris VM上运行与Windows Windows JVM明显不同。默认情况下,Solaris将以"服务器"模式运行,而Windows将以客户端模式运行。 Solaris上的JIT优化将使所有方法访问均相同。
我为四种不同的分发方法创建了一个微基准。 Solaris的结果如下,数量越小,速度越快:
InstanceOf 3156 class== 2925 OO 3083 Id 3067