14

在任何人说什么之前,我只是出于好奇而问这个;我不打算根据这个答案进行任何过早的优化。

我的问题是关于使用反射和投射的速度。标准说法是“反思很慢”。我的问题是到底哪一部分很慢,为什么?特别是在比较某物是否是另一个实例的父项时。

我非常有信心将一个对象的类与另一个 Class 对象进行比较几乎与任何比较一样快,大概只是对已经存储在对象状态中的单例对象进行直接比较;但是如果一个班级是另一个班级的父母呢?

我通常认为instanceof与常规课堂检查一样快,但今天我想了想,似乎必须在“幕后”进行一些反思instanceof才能发挥作用。上网查了一下,发现有几个地方有人说instanceof慢;大概是由于比较对象的父对象需要反射?

这就引出了下一个问题,只是铸造怎么样。如果我将某些东西作为对象投射,我不会得到一个ClassCastException. 但是,如果将对象强制转换为自身的父级,则不会发生这种情况。本质instanceof上,当我在运行时进行演员表时,我正在做一个电话或类似的逻辑,不是吗?我以前从未听过任何人暗示投射物体可能会很慢。诚然,并非所有强制转换都是针对所提供对象的父级的,但很多强制转换都是针对父类的。然而,从来没有人暗示这可能会很慢。

那是什么。真的instanceof没有那么慢吗?两者都instanceof和投射到父类有点慢吗?还是有什么原因可以比instanceof通话更快地完成演员阵容?

4

5 回答 5

19

一如既往地尝试并查看您的特定情况,但是:

- 例外是昂贵的,尤其如此。

- 对代码流使用异常几乎总是一个坏主意

编辑:好的,我很感兴趣,所以我写了一个快速测试系统

public class Test{

    public Test(){
        B b=new B();
        C c=new C();


        for(int i=0;i<10000;i++){
            testUsingInstanceOf(b);
            testUsingInstanceOf(c);
            testUsingException(b);
            testUsingException(c);
        }
    }

    public static void main(String[] args){

        Test test=new Test();

    }

    public static boolean testUsingInstanceOf(A possiblyB){
        if (possiblyB instanceof B){
            return true;
        }else{
            return false;
        }
    }
    public static boolean testUsingException(A possiblyB){
        try{
            B b=(B)possiblyB;
            return true;
        }catch(Exception e){
            return false;
        }
    }


    private class A{

    }
    private class B extends A{

    }
    private class C extends A{

    }        
}

配置文件结果:

by InstanceOf: 4.43 ms
by Exception: 79.4 ms

正如我所说,非常昂贵

即使它总是会成为 B(模拟当你 99% 确定它的 B 时,你只需要确保它仍然没有更快:

始终为 B 时的配置文件结果:

by InstanceOf: 4.48 ms
by Exception: 4.51 ms
于 2013-05-01T14:32:10.533 回答
9

有一个普遍的答案和一个特定的答案。

一般情况

if (/* guard against exception */) {
    /* do something that would throw an exception */
} else {
    /* recover */
}

// versus

try {
   /* do something that would throw an exception */
} catch (TheException ex) {
   /* recover */
}

事实上,创建/抛出/捕获异常是昂贵的。而且它们可能比做测试要贵得多。但是,这并不意味着“测试优先”版本总是更快。这是因为在“测试优先”版本中,实际上可能会执行测试:第一次在 中if,第二次在抛出异常的代码中。

当您考虑到这一点时,很明显,如果(额外)测试的成本足够大并且异常的相对频率足够小,“首先测试”实际上会更慢。例如,在:

if (file.exists() && file.isReadable()) {
    is = new FileInputStream(file);
} else {
    System.err.println("missing file");
}

相对

try {
    is = new FileInputStream(file);
} catch (IOException ex) {
    System.err.println("missing file");
}

“测试优先”方法执行 2 次额外的系统调用,并且系统调用很昂贵。如果“丢失文件”的情况也很不寻常....

第二个混淆因素是最新的 HotSpot JIT 编译器对异常进行了一些重要的优化。特别是,如果 JIT 编译器能够判断出异常对象的状态未被使用,它可能会将异常创建/抛出/捕获变成简单的跳转指令。

具体案例instanceof

在这种情况下,我们很可能比较这两者:

if (o instanceof Foo) {
    Foo f = (Foo) o;
    /* ... */
} 

// versus

try {
    Foo f = (Foo) o;
} catch (ClassCastException ex) {
   /* */
}

这里发生了第二次优化。instanceofa 后跟 a是一种type cast常见的模式。HotSpot JIT 编译器通常可以消除类型转换执行的动态类型检查......因为这是重复刚刚成功的测试。当您考虑到这一点时,“测试优先”版本不能比“异常”版本慢……即使后者被优化为跳转。

于 2013-05-01T15:24:22.693 回答
4

不幸的是,Richard 的代码不能直接运行以产生计时。我稍微修改了这个问题,假设你真的想要“如果 A 是 B,就在 B 上做点什么”,而不是仅仅问“A 是 B 吗?”。这是测试代码。

从原始更新编写 HotSpot 编译器并没有减少到零的琐碎代码相当具有挑战性,但我认为以下内容很好:

package net.redpoint.utils;
public class Scratch {
    public long counter = 0;
    public class A { 
        public void inc() { counter++; } 
    }
    public class B extends A { 
        public void inc() { counter++; } 
    }
    public class C extends A {
        public void inc() { counter++; } 
    } 
    public A[] a = new A[3];
    public void test() {
        a[0] = new A();
        a[1] = new B();
        a[2] = new C();
        int iter = 100000000;
        long start = System.nanoTime();
        for(int i = iter; i > 0; i--) {
            testUsingInstanceOf(a[i%3]);
        }
        long end = System.nanoTime();
        System.out.println("instanceof: " + iter / ((end - start) / 1000000000.0) + " per second");

        start = System.nanoTime();
        for(int i = iter; i > 0; i--) {
            testUsingException(a[i%3]);
        }
        end = System.nanoTime();
        System.out.println("try{}: " + iter / ((end - start) / 1000000000.0) + " per second");

        start = System.nanoTime();
        for(int i = iter; i > 0; i--) {
            testUsingClassName(a[i%3]);
        }
        end = System.nanoTime();
        System.out.println("classname: " + iter / ((end - start) / 1000000000.0) + " per second");
    }

    public static void main(String[] args) {
        Scratch s = new Scratch();
        s.test();
    }

    public void testUsingInstanceOf(A possiblyB){
        if (possiblyB instanceof B){
            ((B)possiblyB).inc();
        }
    }

    public void testUsingException(A possiblyB){
        try{
            ((B)possiblyB).inc();
        } catch(Exception e){
        }
    }

    public void testUsingClassName(A possiblyB){
        if (possiblyB.getClass().getName().equals("net.redpoint.utils.Scratch$B")){
            ((B)possiblyB).inc();
        }
    }
}

结果输出:

instanceof: 4.573174070960945E8 per second
try{}: 3.926650051387284E8 per second
classname: 7.689439655530204E7 per second

测试是在 Windows 8 x64 上使用 Oracle JRE7 SE 和 Intel i7 沙桥 CPU 执行的。

于 2015-03-04T15:45:10.577 回答
1

如果演员表可以抛出异常,这意味着它正在隐含地做instanceof会做的事情。因此,在这两种情况下,您都在隐式使用反射,可能以完全相同的方式。

不同之处在于,如果instanceof返回结果false,则不再发生反射。如果强制转换失败并抛出异常,您将展开执行堆栈,并且很可能会有更多反射(运行时catch根据抛出的异常对象是否是类型的实例来确定正确的块)被抓)。

上面的逻辑告诉我instanceof检查应该更快。当然,针对您的特定情况进行基准测试会给您明确的答案。

于 2013-05-01T14:43:48.967 回答
-2

我认为您不会发现其中一个明显优于另一个。

对于instanceof,完成的工作使用内存和 cpu 时间。创建异常也会使用内存和 CPU 时间。每个使用较少,只有一个做得好的基准会给你这个答案。

编码方面,我更愿意看到 ainstanceof而不是强制转换和必须管理异常。

于 2013-05-01T14:35:38.087 回答