具有不同 class 继承级别的 getDeclaredMethod() 和 getDeclaredMethods() 之间的性能差异
Performance difference between getDeclaredMethod() and getDeclaredMethods() with different levels of class inheritance
希望在Android中找到一个方法的声明class。基本思想是查找目标方法是否在class中声明。如果没有,我会递归地在它的超 classes 中找到目标方法。出于特定原因,效率很重要。所以我有两个实现,我比较了它们的性能差异。
版本 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;
}
版本 2:使用 Class.getDeclaredMethods
手动检查方法是否在 class 中声明。每个方法我们都会根据方法名和参数类型一一匹配。
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;
}
我在测试这两个版本的效率时有一些有趣的观察。基本上我创建了几个空的 classes C
、CC
、CCC
、CCCC
。他们的关系是
CC extends C
CCC extends CC
CCCC extends CCC
所有这些 classes 没有声明任何方法,我使用 toString
方法作为目标方法。我用 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");
结果如下:
cls
Version 1
Version 2
C
1168ms
1632ms
CC
2599ms
1397ms
CCC
3495ms
1680ms
CCCC
4908ms
1559ms
我们可以看到,当我们拥有更多级别的 class 继承时,版本 1 的性能会显着下降。但是版本2的性能变化不大。
那么版本 1 和版本 2 之间有什么区别呢?是什么让版本 1 这么慢?
您的测量方法不是有效的基准。一些问题可以在 How do I write a correct micro-benchmark in Java?
中找到
但是,虽然结果可能存在错误或噪音,但它们并非不可信。
您创建了没有方法的测试 classes,因此 getDeclaredMethods()
将 return 一个空数组。这意味着对于这些 classes,第二个变体的循环体中发生的任何事情都无关紧要——它永远不会执行。遍历空数组几乎不花时间,所以实际时间总是花在最后 class、java.lang.Object
上,它声明了方法,包括您要搜索的方法。
相比之下,第一个变体每次都构造并传递一个新的NoSuchMethodException
,该方法还没有在class中找到。只要这种开销没有得到优化(你不应该依赖它来实现),它就是一个相当昂贵的操作。
如 The Exceptional Performance of Lil' Exception 中所述,很大一部分成本是构建堆栈跟踪,这取决于调用堆栈的深度。由于您的方法以递归方式处理 class 层次结构,因此随着 class 层次结构的加深,这些成本比线性增长更糟。
这是一个实现细节,我不知道Android的环境是如何实现的,但是getDeclaredMethod
不一定比线性搜索更好。对于 OpenJDK,它不会,因为它仍在后台执行线性搜索。构建允许 better-than-linear 查找的数据结构本身会花费时间,但只有在应用程序对同一个 class 执行大量反射查找时才会得到回报,这种情况很少发生。
因此,对于此类实现,您可以在任一变体中进行线性搜索。第二个具有克隆方法数组的成本,因为 getDeclaredMethods()
return 是防御性副本,但这远低于第一个变体中创建异常的成本。
在这样的操作中,当您希望该方法在某些 class 层次结构中不存在时,您最好使用手动搜索,以防止出现异常。
不过,你可以简化操作:
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;
}
这使用循环而不是递归和已经存在的数组比较方法。但是它对数组长度做了一个 pre-test 。因此,当长度已经不同时,它避免了创建参数类型数组的防御性副本。这可能并不总是必要的,具体取决于运行时环境的优化能力,但如您所说,将其包含在对性能至关重要的代码中也无妨。
希望在Android中找到一个方法的声明class。基本思想是查找目标方法是否在class中声明。如果没有,我会递归地在它的超 classes 中找到目标方法。出于特定原因,效率很重要。所以我有两个实现,我比较了它们的性能差异。
版本 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;
}
版本 2:使用 Class.getDeclaredMethods
手动检查方法是否在 class 中声明。每个方法我们都会根据方法名和参数类型一一匹配。
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;
}
我在测试这两个版本的效率时有一些有趣的观察。基本上我创建了几个空的 classes C
、CC
、CCC
、CCCC
。他们的关系是
CC extends C
CCC extends CC
CCCC extends CCC
所有这些 classes 没有声明任何方法,我使用 toString
方法作为目标方法。我用 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");
结果如下:
cls | Version 1 | Version 2 |
---|---|---|
C | 1168ms | 1632ms |
CC | 2599ms | 1397ms |
CCC | 3495ms | 1680ms |
CCCC | 4908ms | 1559ms |
我们可以看到,当我们拥有更多级别的 class 继承时,版本 1 的性能会显着下降。但是版本2的性能变化不大。
那么版本 1 和版本 2 之间有什么区别呢?是什么让版本 1 这么慢?
您的测量方法不是有效的基准。一些问题可以在 How do I write a correct micro-benchmark in Java?
中找到但是,虽然结果可能存在错误或噪音,但它们并非不可信。
您创建了没有方法的测试 classes,因此 getDeclaredMethods()
将 return 一个空数组。这意味着对于这些 classes,第二个变体的循环体中发生的任何事情都无关紧要——它永远不会执行。遍历空数组几乎不花时间,所以实际时间总是花在最后 class、java.lang.Object
上,它声明了方法,包括您要搜索的方法。
相比之下,第一个变体每次都构造并传递一个新的NoSuchMethodException
,该方法还没有在class中找到。只要这种开销没有得到优化(你不应该依赖它来实现),它就是一个相当昂贵的操作。
如 The Exceptional Performance of Lil' Exception 中所述,很大一部分成本是构建堆栈跟踪,这取决于调用堆栈的深度。由于您的方法以递归方式处理 class 层次结构,因此随着 class 层次结构的加深,这些成本比线性增长更糟。
这是一个实现细节,我不知道Android的环境是如何实现的,但是getDeclaredMethod
不一定比线性搜索更好。对于 OpenJDK,它不会,因为它仍在后台执行线性搜索。构建允许 better-than-linear 查找的数据结构本身会花费时间,但只有在应用程序对同一个 class 执行大量反射查找时才会得到回报,这种情况很少发生。
因此,对于此类实现,您可以在任一变体中进行线性搜索。第二个具有克隆方法数组的成本,因为 getDeclaredMethods()
return 是防御性副本,但这远低于第一个变体中创建异常的成本。
在这样的操作中,当您希望该方法在某些 class 层次结构中不存在时,您最好使用手动搜索,以防止出现异常。
不过,你可以简化操作:
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;
}
这使用循环而不是递归和已经存在的数组比较方法。但是它对数组长度做了一个 pre-test 。因此,当长度已经不同时,它避免了创建参数类型数组的防御性副本。这可能并不总是必要的,具体取决于运行时环境的优化能力,但如您所说,将其包含在对性能至关重要的代码中也无妨。