338

我正在开发一个应用程序,一种设计方法涉及对instanceof运算符的大量使用。虽然我知道 OO 设计通常会尽量避免使用instanceof,但这是另一回事,这个问题完全与性能有关。我想知道是否有任何性能影响?是一样快==吗?

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

我想到的解决它的其他方法之一是改用“type id”整数原语,并使用位掩码来表示子类的类别,然后将子类“type id”与一个位掩码进行比较表示类别的常量掩码。

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

4

24 回答 24

319

方法

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

  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 |
|------------|------------------------------------ --|------------------------|
| 实例 | 39,598 ± 0,022 ns/操作 | 100,00 % |
| 获取类 | 39,687 ± 0,021 ns/操作 | 100,22 % |
| 类型 | 46,295 ± 0,026 纳秒/操作 | 116,91 % |
| 面向对象 | 48,078 ± 0,026 ns/操作 | 121,42 % |

tl;博士

在 Java 1.8instanceof中是最快的方法,虽然getClass()非常接近。

于 2014-10-22T18:54:40.517 回答
288

现代 JVM/JIT 编译器已经消除了大多数传统上“慢”操作的性能损失,包括 instanceof、异常处理、反射等。

正如 Donald Knuth 所写,“我们应该忘记小的效率,比如大约 97% 的时间:过早的优化是万恶之源。” instanceof 的性能可能不会成为问题,所以在确定问题所在之前,不要浪费时间想出奇特的解决方法。

于 2008-09-19T16:45:45.170 回答
76

我只是做了一个简单的测试,看看 instanceOf 的性能如何与对只有一个字母的字符串对象的简单 s.equals() 调用进行比较。

在 10.000.000 循环中,instanceOf 给了我 63-96ms,而字符串 equals 给了我 106-230ms

我使用了java jvm 6。

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

使用整数的 .equals() 而不是字符串给了我相同的结果,只有当我使用 == i 比 instanceOf 快 20 毫秒时(在 10.000.000 循环中)

于 2008-12-29T12:13:26.450 回答
20

将确定性能影响的项目是:

  1. instanceof 运算符可以返回 true 的可能类的数量
  2. 您的数据分布 - 大多数 instanceof 操作是否在第一次或第二次尝试中解决?您需要将最有可能返回真正的操作放在首位。
  3. 部署环境。在 Sun Solaris VM 上运行与 Sun 的 Windows JVM 有很大不同。默认情况下,Solaris 将在“服务器”模式下运行,而 Windows 将在客户端模式下运行。Solaris 上的 JIT 优化将使所有方法的访问权限都相同。

为四种不同的调度方法创建了一个微基准。来自 Solaris 的结果如下,数字越小越快:

InstanceOf 3156
class== 2925 
OO 3083 
Id 3067 
于 2009-11-02T03:35:56.720 回答
19

回答您的最后一个问题:除非分析器告诉您,您在 instanceof 中花费了大量时间:是的,您在吹毛求疵。

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

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

并且本着这个答案的真实精神(我完全相信):一旦 jit 编译器有机会优化它,我绝对不知道 instanceof 和 == 之间的关系。

我忘记了:永远不要测量第一次运行。

于 2008-09-19T16:51:00.260 回答
16

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

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()));
    }
}
于 2012-03-23T19:06:10.940 回答
13

instanceof速度非常快,只需要几条 CPU 指令。

显然,如果一个类X没有加载子类(JVM 知道),instanceof可以优化为:

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

主要成本只是阅读!

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

大家好消息!

于 2011-07-16T10:13:26.027 回答
6

在大多数现实世界的实现中, instanceof 可能会比简单的 equals 更昂贵(也就是说,真正需要 instanceof 的那些,并且您不能仅仅通过覆盖一个通用方法来解决它,就像每个初学者教科书以及上面的德米安建议)。

这是为什么?因为可能会发生的是你有几个接口,它们提供一些功能(比如说,接口 x、y 和 z),以及一些可能(或不)实现这些接口之一的要操作的对象......但是不直接。比如说,我有:

w 扩展 x

A 实现 w

B 扩展 A

C 扩展 B,实现 y

D 扩展 C,实现 z

假设我正在处理 D 的一个实例,即对象 d。计算 (d instanceof x) 需要使用 d.getClass(),循环遍历它实现的接口以了解一个是否 == 到 x,如果不是,则再次递归地对其所有祖先执行此操作......在我们的例子中,如果您对该树进行广度优先探索,则至少会产生 8 次比较,假设 y 和 z 不扩展任何内容...

现实世界的派生树的复杂性可能更高。在某些情况下,如果 JIT 能够提前将 d 解析为,在所有可能的情况下,它是扩展 x 的某个实例,则 JIT 可以优化其中的大部分内容。然而,实际上,您大部分时间都将经历该树遍历。

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

这是我的 2 美分,希望对您有所帮助...

于 2009-01-27T18:30:28.367 回答
5

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

于 2008-09-19T16:49:34.663 回答
5

instanceof 非常有效,因此您的性能不太可能受到影响。但是,使用大量 instanceof 表明存在设计问题。

如果您可以使用 xClass == String.class,这会更快。注意:最终类不需要 instanceof。

于 2009-01-27T20:07:04.457 回答
5

我基于 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();
}
}
于 2019-06-22T05:07:32.310 回答
4

通常,在这种情况下(instanceof 正在检查此基类的子类)不赞成“instanceof”运算符的原因通常是因为您应该做的是将操作移动到方法中并为适当的方法覆盖它子类。例如,如果您有:

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

您可以将其替换为

o.doEverything();

然后在 Class1 中调用“doThis()”中的“doEverything()”,在 Class2 中调用“doThat()”,以此类推。

于 2008-09-19T16:46:05.090 回答
4

'instanceof' 实际上是一个运算符,就像 + 或 - 一样,我相信它有自己的 JVM 字节码指令。它应该很快。

我不应该说,如果您有一个要测试对象是否是某个子类的实例的开关,那么您的设计可能需要重新设计。考虑将特定于子类的行为下推到子类本身。

于 2008-09-19T16:46:56.197 回答
4

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

if (o instanceof java.lang.String)

可能与以下 C 代码一样快

if (objectStruct->iAmInstanceOf == &java_lang_String_class)

假设 JIT 编译器已经到位并且做得不错。

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

但是,它不必如此,它在很大程度上取决于 JVM。但是,如果这会成为您代码中的瓶颈操作,我会认为 JVM 实现相当糟糕。即使没有 JIT 编译器并且只解释代码,也应该能够几乎立即进行 instanceof 测试。

于 2008-09-19T16:58:13.753 回答
4

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

我非常喜欢可以以多种方式使用的小型数据对象。如果您遵循覆盖(多态)方法,则您的对象只能以“一种方式”使用。

这就是模式出现的地方......

您可以使用双重分派(如在访问者模式中)要求每个对象“呼叫您”传递自身——这将解析对象的类型。但是(再次)您需要一个可以使用所有可能的子类型“做事”的类。

我更喜欢使用策略模式,您可以在其中为要处理的每个子类型注册策略。类似于以下内容。请注意,这仅有助于精确的类型匹配,但具有可扩展的优势 - 第三方贡献者可以添加自己的类型和处理程序。(这对于像 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);
    }
}
于 2008-09-21T15:51:16.937 回答
3

我会在 instanceof 性能方面与您联系。但是完全避免问题(或缺乏问题)的一种方法是为您需要在其上执行 instanceof 的所有子类创建一个父接口。该接口将是您需要对其进行 instanceof 检查的子类中所有方法的超集。如果方法不适用于特定的子类,只需提供此方法的虚拟实现即可。如果我没有误解这个问题,这就是我过去解决问题的方法。

于 2013-05-30T21:59:34.177 回答
2

在现代 Java 版本中,instanceof 运算符作为简单的方法调用更快。这表示:

if(a instanceof AnyObject){
}

更快:

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

另一件事是如果您需要级联许多instanceof。然后只调用一次 getType() 的开关更快。

于 2008-09-19T19:09:53.077 回答
1

InstanceOf是对糟糕的面向对象设计的警告。

当前的 JVM 确实意味着instanceOf本身并没有太大的性能问题。如果您发现自己经常使用它,尤其是在核心功能方面,那么可能是时候看看设计了。重构为更好的设计所带来的性能(和简单性/可维护性)收益将大大超过实际调用instanceOf所花费的任何实际处理器周期。

举一个非常简单的编程示例。

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

一个糟糕的架构是一个更好的选择,让 SomeObject 成为两个子类的父类,其中每个子类都覆盖一个方法(doSomething),因此代码如下所示:

Someobject.doSomething();
于 2008-09-19T17:15:09.047 回答
1

如果速度是您的唯一目标,那么使用 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% 的总速度。

于 2011-07-27T14:13:43.073 回答
0

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

您应该详细说明该问题,如果您只对具体类型感兴趣,也许您可​​以使用关联存储,例如 Map<Class, Object>。

于 2008-09-19T16:50:42.693 回答
0

关于 Peter Lawrey 的注释,即最终类不需要 instanceof 并且可以只使用引用相等,请小心!即使最终类不能扩展,也不能保证它们被同一个类加载器加载。如果您绝对肯定该部分代码只有一个类加载器,请仅使用 x.getClass() == SomeFinal.class 或其同类。

于 2009-02-19T01:25:01.620 回答
0

我也更喜欢枚举方法,但我会使用抽象基类来强制子类实现该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;
    }
  }
}
于 2013-08-13T15:38:05.477 回答
0

我认为可能值得提交一个反例来反对此页面上的普遍共识,即“instanceof”的成本不足以担心。我发现我在内部循环中有一些代码(在一些历史性的优化尝试中)确实

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

在 SingleItem 上调用 head() 会返回未更改的值。将代码替换为

seq = seq.head();

让我从 269 毫秒加速到 169 毫秒,尽管在循环中发生了一些相当繁重的事情,比如字符串到双精度的转换。当然,加速可能更多是由于消除了条件分支而不是消除了 instanceof 运算符本身。但我认为值得一提。

于 2017-10-16T07:53:53.673 回答
-4

你专注于错误的事情。instanceof 和任何其他检查同一事物的方法之间的差异可能甚至无法衡量。如果性能至关重要,那么 Java 可能是错误的语言。主要原因是您无法控制 VM 何时决定收集垃圾,这可能会使大型程序中的 CPU 达到 100% 几秒钟(MagicDraw 10 非常适合)。除非您能控制该程序将在其上运行的每台计算机,否则您无法保证它将在哪个版本的 JVM 上运行,并且许多较旧的 JVM 存在严重的速度问题。如果它是一个小型应用程序,你可能对 Java 没问题,但如果你不断地读取和丢弃数据,那么你注意到 GC 何时启动。

于 2008-09-19T16:51:11.960 回答