简体   繁体   English

getDeclaredMethod() 和 getDeclaredMethods() 不同级别的性能差异 class inheritance

[英]Performance difference between getDeclaredMethod() and getDeclaredMethods() with different levels of class inheritance

I hope to find the declaring class of a method in Android. The basic idea is to find if the target method is declared in a class. If not, I will recursively find the target method in its super classes.我希望在Android中找到一个方法的声明class。基本思路是查找目标方法是否在class中声明。如果没有,我将在其超类中递归查找目标方法。 For a specific reason, efficiency matters.出于特定原因,效率很重要。 So I have two implementations and I compared their performance differences.所以我有两个实现,我比较了它们的性能差异。

Version 1 : use the API Class.getDeclaredMethod to check if a method exists in a class. If not, NoSuchMethodException will be thrown.版本 1 :使用 API Class.getDeclaredMethod检查 class 中是否存在方法。如果不存在,将抛出NoSuchMethodException

public Class getDeclaringClass1(Class cls, String mtdName, Class[] paraTypes) {
        Class declaringCls = null;
        try {
            declaringCls = cls.getDeclaredMethod(mtdName, paraTypes).getDeclaringClass();
        } catch (NoSuchMethodException | SecurityException e) {
            Class supr = cls.getSuperclass();
            if(supr != null) declaringCls = getDeclaringClass1(supr, mtdName, paraTypes);
        }
        return declaringCls;
}

Version 2 : Manually check if a method is declared in a class using Class.getDeclaredMethods .版本 2 :使用Class.getDeclaredMethods手动检查方法是否在 class 中声明。 We will match each method one by one with the method name and parameter types.我们将每个方法与方法名称和参数类型一一匹配。

public Class getDeclaringClass2(Class cls, String mtdName, Class[] paraTypes) {
        Class declaringCls = null;
        Method[] methods = cls.getDeclaredMethods();
        boolean containsMtd = false;
        for(Method method: methods) {
            if(method.getName().equals(mtdName)) {
                boolean allEqual = true;
                for(int i=0; i< method.getParameterTypes().length; i++) {
                    if(! method.getParameterTypes()[i].equals(paraTypes[i])) {
                       allEqual = false;
                       break;
                    }
                }
                if(allEqual) {
                    containsMtd = true;
                    declaringCls = cls;
                    break;
                }
            }
        }
        if(! containsMtd) {
           Class supr = cls.getSuperclass();
           if(supr != null) declaringCls = getDeclaringClass2(supr, mtdName, paraTypes);
        }
        return declaringCls;
}

I made some interesting observations when testing the efficiency of these two versions.在测试这两个版本的效率时,我做了一些有趣的观察。 Basically I created several empty classes C , CC , CCC , CCCC .基本上我创建了几个空类CCCCCCCCCC Their relationships are他们的关系是

CC extends C
CCC extends CC
CCCC extends CCC

All these classes does not declare any methods and I use the toString method as the target method.所有这些类都没有声明任何方法,我使用toString方法作为目标方法。 I testing two getDeclaringClass with a loop of 10000 times:我用 10000 次循环测试两个getDeclaringClass

start = System.currentTimeMillis();
for(long i=0; i<10000; i++) {
     getDeclaringClass(cls, "toString", new Class[0]).getName()); // getDeclaringClass will be set to version 1 or 2
}
end = System.currentTimeMillis();
System.out.println((end - start) + "ms");

Here are the results:以下是结果:

cls控制系统 Version 1版本 1 Version 2版本 2
C C 1168ms 1168毫秒 1632ms 1632毫秒
CC CC 2599ms 2599毫秒 1397ms 1397毫秒
CCC CCC 认证 3495ms 3495毫秒 1680ms 1680毫秒
CCCC中国交建 4908ms 4908毫秒 1559ms 1559毫秒

We can see that version 1's performance drops significantly when we have more levels of class inheritance. But version 2's performance does not change a lot.我们可以看到,当我们有更多级别 class inheritance 时,版本 1 的性能明显下降。但是版本 2 的性能变化不大。

So what makes the difference between version 1 and 2?那么版本 1 和版本 2 之间有什么区别呢? What makes version 1 so slow?是什么让版本 1 如此缓慢?

Your method of measuring is not a valid benchmark.您的测量方法不是有效的基准。 Some of the issues can be found in How do I write a correct micro-benchmark in Java?一些问题可以在How do I write a correct micro-benchmark in Java? 中找到。

But while the results are subject to errors or noise, they are not implausible.但是,尽管结果可能存在错误或噪音,但它们并非不可信。

You have created test classes with no methods, so getDeclaredMethods() will return an empty array.您创建了没有方法的测试类,因此getDeclaredMethods()将返回一个空数组。 This implies that nothing happening in the loop body of your second variant matters at all for these classes—it's never executed.这意味着对于这些类,第二个变体的循环体中发生的任何事情都无关紧要——它永远不会执行。 Iterating over an empty array takes almost no time at all, so the actual time is always spend in the last class, java.lang.Object , which has declared methods, including the one you're searching for.遍历一个空数组几乎不花时间,所以实际时间总是花在最后一个 class, java.lang.Object ,它声明了方法,包括你正在搜索的方法。

In contrast, the first variant constructs and delivers a new NoSuchMethodException every time, the method has not been found in a class. As long as this overhead does not get optimized away (and you should not rely on this to happen), it's a rather expensive operation.相比之下,第一个变体每次都构造并传递一个新的NoSuchMethodException ,该方法还没有在 class 中找到。只要这个开销没有得到优化(并且你不应该依赖它来发生),它是一个相当昂贵的操作。

As explained in The Exceptional Performance of Lil' Exception , a significant portion of the costs is the construction of the stack trace, which depends on the depth of the call stack.正如 Lil'Exception的卓越性能中所解释的,很大一部分成本是堆栈跟踪的构造,这取决于调用堆栈的深度。 Since your method processes the class hierarchy recursively, these costs grow worse than linear with deeper class hierarchies.由于您的方法以递归方式处理 class 层次结构,因此随着 class 层次结构的深入,这些成本比线性增长更糟。

It's an implementation detail and I don't know how Android's environment has implemented it, but getDeclaredMethod does not necessarily work better than a linear search.这是一个实现细节,我不知道 Android 的环境是如何实现它的,但是getDeclaredMethod不一定比线性搜索更好。 In case of OpenJDK it does not, as it is still performing a linear search under the hood.对于 OpenJDK,它不会,因为它仍在后台执行线性搜索。 Building a data structure allowing a better-than-linear lookup would cost time on its own but only pay off, if an application is performing a lot of reflective lookups on the same class, which rarely happens.构建允许优于线性查找的数据结构本身会花费时间,但只有在应用程序对同一个 class 执行大量反射查找时才会得到回报,这种情况很少发生。

So for such implementations, you have a linear search in either variant.因此,对于此类实现,您可以在任一变体中进行线性搜索。 The second has the costs of cloning the array of methods, as getDeclaredMethods() returns a defensive copy, but this is far less than the costs of the creation of exceptions in the first variant.第二个具有克隆方法数组的成本,因为getDeclaredMethods()返回防御副本,但这远低于第一个变体中创建异常的成本。

In an operation like this, when you expect the method to be absent in some classes of the hierarchy, you are better of with the manual search, preventing exceptions.在这样的操作中,当您希望该方法在层次结构的某些类中不存在时,您最好使用手动搜索,以防止出现异常。

Still, you can simplify the operation:不过,您可以简化操作:

public Class<?> getDeclaringClass2(Class<?> cls, String mtdName, Class<?>[] paraTypes) {
    for(; cls != null; cls = cls.getSuperclass()) {
        for(Method method: cls.getDeclaredMethods()) {
            if(method.getName().equals(mtdName)
            && method.getParameterCount() == paraTypes.length
            && Arrays.equals(method.getParameterTypes(), paraTypes)) 
                return cls;
        }
    }
    return null;
}

This uses a loop rather than recursion and the already existing array comparison method.这使用循环而不是递归和已经存在的数组比较方法。 But it does a pre-test for the array length.但它对数组长度进行了预测试。 So when the lengths already differ, it avoids the creation of a defensive copy of the parameter types array.因此,当长度已经不同时,它避免了创建参数类型数组的防御性副本。 This might not always be necessary, depending on the optimization capabilities of the runtime environment, but it won't hurt to be included in code which, as you said, is performance critical.这可能并不总是必要的,具体取决于运行时环境的优化能力,但如您所说,将其包含在对性能至关重要的代码中也无妨。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM