该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2009-09-27
定位到一个java方法,其实只需要类型(Class),方法名及参数即可。
|
|
返回顶楼 | |
发表时间:2009-09-27
Saito 写道 treblesoftware 写道 怎么 怎么 怎么 ,第一个例子怎么越看越像反射。
童鞋是不是再仔细看看整篇文章? .. btw: 老赵也就是看见Twitter来一下. 不看见Twitter就窝在blogcn码字..多无趣啊.. 其实,我只是调解一下紧张的气氛,才这么说的。 |
|
返回顶楼 | |
发表时间:2009-09-27
反射和MethodHandle差10倍,那MethodHandle和直接的调用差多少?
|
|
返回顶楼 | |
发表时间:2009-09-27
JohnnyJian 写道 反射和MethodHandle差10倍,那MethodHandle和直接的调用差多少?
果然要问这个问题么……嘛,测一下也不是不行。 不过仍然需要强调的是,JDK 7里的MethodHandle的内部设计与API设计都还没定案,还在不断改进中。HotSpot目前对MethodHandle.invoke的内联支持也还不彻底,所以拿现在的MethodHandle跟直接调用来比较会有明显的差距。即便如此它已经比普通的反射调用要快很多。最终的目标是让MethodHandle.invoke跟接口方法调用的速度差不多。 那么废话少说,上代码: 直接调用静态方法: public class SpeedTrap { private static void doNothing(int x, int y, int z) { } private static void test() { for (int i = 0; i < 100000; i++) { doNothing(1, 2, 3); } } public static void main(String[] args) { // warm up for (int i = 0; i < 10; i++) { test(); } // time the test long start = System.nanoTime(); test(); long end = System.nanoTime(); System.out.println("elapse time: " + (end - start)); } } 直接调用接口方法: interface Callable3 { void call(int x, int y, int z); } class Callable3Impl implements Callable3 { public void call(int x, int y, int z) { } } public class SpeedTrap3 { private static void test(Callable3 c) { for (int i = 0; i < 100000; i++) { c.call(1, 2, 3); } } public static void main(String[] args) { Callable3 c = new Callable3Impl(); // warm up for (int i = 0; i < 10; i++) { test(c); } // time the test long start = System.nanoTime(); test(c); long end = System.nanoTime(); System.out.println("elapse time: " + (end - start)); } } 前面的我给出的MethodHandle与普通反射的比较,用的例子是针对静态方法为目标的调用。实际上直接调用静态方法算是HotSpot里最容易优化的一种调用了,所以测试耗时很短: 引用 elapse time: 134933
elapse time: 134933 elapse time: 134934 elapse time: 134934 elapse time: 135213 相比之下,接口方法调用就慢一些, 引用 elapse time: 469054
elapse time: 468495 elapse time: 475759 elapse time: 468496 elapse time: 468775 MethodHandle.invoke最后就应该能达到接近这个水平。 为什么这两组测试比前面两组测试快那么多呢?因为我们要测试的“对象”——方法调用消失了。继续看代码, 静态方法调用版的test方法: ;; 函数入口处理(prologue) 0x00be6890: mov %eax,-0x4000(%esp) 0x00be6897: push %ebp 0x00be6898: mov %esp,%ebp 0x00be689a: sub $0x18,%esp ;*iconst_0 ; - SpeedTrap::test@0 (line 5) ;; 函数体开始 ;; 循环初始化 0x00be689d: mov $0x0,%esi 0x00be68a2: jmp 0x00be68af ;*istore_0 ; - SpeedTrap::test@1 (line 5) 0x00be68a7: nop ;; 循环体开始 ;; doNothing()方法的调用被内联进来而消失了 0x00be68a8: inc %esi ; OopMap{off=25} ;*goto ; - SpeedTrap::test@17 (line 5) 0x00be68a9: test %eax,0x990100 ;*goto ; - SpeedTrap::test@17 (line 5) ; {poll} ;; 循环条件 0x00be68af: cmp $0x186a0,%esi 0x00be68b5: jl 0x00be68a8 ;*if_icmpge ; - SpeedTrap::test@5 (line 5) ;; 函数出口处理(epilogue) 0x00be68b7: mov %ebp,%esp 0x00be68b9: pop %ebp 0x00be68ba: test %eax,0x990100 ; {poll_return} 0x00be68c0: ret 接口方法调用版的test方法: ;; 函数入口处理(prologue) 0x00be7230: mov %eax,-0x4000(%esp) 0x00be7237: push %ebp 0x00be7238: mov %esp,%ebp 0x00be723a: sub $0x18,%esp ;*iconst_0 ; - SpeedTrap3::test@0 (line 11) ;; 函数体开始 ;; 循环初始化 0x00be723d: mov $0x0,%esi 0x00be7242: jmp 0x00be726c ;*istore_1 ; - SpeedTrap3::test@1 (line 11) 0x00be7247: nop ;; 循环体开始 0x00be7248: cmp $0x0,%ecx ; 空指针检查(检查参数c是否为空) 0x00be724b: je 0x00be7261 ; 空指针时跳转到0x00be7261 0x00be7251: mov 0x4(%ecx),%ebx ; 这条与下条指令检查c的类型是否为Callable3Impl 0x00be7254: cmpl $0x14230e10,0x20(%ebx) ; {oop('Callable3Impl')} 0x00be725b: jne 0x00be727e ; c不是类型的实例则跳转到0x00be727e 0x00be7261: mov %ecx,%edi 0x00be7263: cmp (%ecx),%eax ;*invokeinterface call ; - SpeedTrap3::test@12 (line 12) ; implicit exception: dispatches to 0x00be7294 ;; 实际的c.call()的调用被内联进来而消失 0x00be7265: inc %esi ; OopMap{ecx=Oop off=54} ;*goto ; - SpeedTrap3::test@20 (line 11) 0x00be7266: test %eax,0x990100 ;*goto ; - SpeedTrap3::test@20 (line 11) ; {poll} ;; 循环条件 0x00be726c: cmp $0x186a0,%esi 0x00be7272: jl 0x00be7248 ;*if_icmpge ; - SpeedTrap3::test@5 (line 11) ;; 函数出口处理(epilogue) 0x00be7274: mov %ebp,%esp 0x00be7276: pop %ebp 0x00be7277: test %eax,0x990100 ; {poll_return} 0x00be727d: ret 这次就不写那么详细的注释了,相信参考之前的代码也可以理解个大概。 关键点就是:原本应该有call指令进行方法调用的地方,现在消失了。这就是方法内联的效果。因为被内联的是空方法,内联进来之后自然是什么也不留下了。 由于静态方法不参与继承/重写相关的多态,可以说是“编译时确定的目标”,所以静态方法是最容易内联的,不需要做额外的检查。 而虚方法/接口方法则实际调用的版本取决于receiver的类型,要内联的话就必须要做一定检查: ·如果只记录前一次调用遇到的receiver类型(或其它影响dispatch的信息),这种callsite cache就叫做monomorphic inline cache,简称MIC; ·如果记录之前多次调用遇到的receiver类型(或其它影响dispatch的信息),这种callsite cache就叫做polymorphic inline cache,简称PIC。 还有所谓megamorphic状态,一般是指receiver变化太多,不值得做inline caching,而总是采取较慢的传统方式搜索目标方法。 上面的接口方法调用测试中展现的就是MIC:先检查receiver类型是否为某个已知类型(Callable3Impl),如果是的话就直接执行内联版本的c.call();否则退回到搜索方法的逻辑,并视情况决定是否更新或取消MIC。 正是因为MethodHandle.invoke在目前的JDK 7中尚未彻底实现inline功能,所以其开销比接口方法调用还是大很多。不过有两个工程师已经在努力实现相关功能了,可以期待以后的性能改善。 |
|
返回顶楼 | |
发表时间:2009-09-28
没有jsr292的时候,用beanshell不是可以很好的模拟对于动态语言的支持么?
|
|
返回顶楼 | |
发表时间:2009-09-28
还是函数指针好
什么时候java把指针也放出来得了 |
|
返回顶楼 | |
发表时间:2009-09-28
unsid 写道 没有jsr292的时候,用beanshell不是可以很好的模拟对于动态语言的支持么? BeanShell是JVM上的“一种”动态脚本语言。JRuby、Jython、Rhino这些语言的实现不太可能依靠BeanShell的动态能力去解决。JSR 292就是为了让这些语言更容易实现,实现出来更高效而设计的。我在顶楼的帖的最后留了篇Charles Nutter讲在invokedynamic出现之前实现JRuby的一些麻烦的地方,可以读一下。需要翻墙,这个请自行解决~ |
|
返回顶楼 | |
发表时间:2009-09-28
最后修改:2009-09-28
Java代码
findStatic( TestMethodHandle1.class, // 方法所属类型(Class) "hello", // 方法名 type // 由参数和返回值类型组成的“方法类型” ); type// 由参数和返回值类型组成的“方法类型” “方法类型”这个参数设计得很失败, 一个类中的方法,如果方法名和参数个数及类型一样,这个类能正确编译吗? 其实MethodHandle最终只需要暴露类似这样一个静态方法即可: //方法如果没有返回值,为void MethodHandle.<T>invoke( Class clazz, // 方法所属类型(Class) String methodName, // 方法名 Object... params//方法参数,可以运行时确定参数个数及类型,定位到具体方法 ) |
|
返回顶楼 | |
发表时间:2009-09-28
star022 写道 Java代码
findStatic( TestMethodHandle1.class, // 方法所属类型(Class) "hello", // 方法名 type // 由参数和返回值类型组成的“方法类型” ); type// 由参数和返回值类型组成的“方法类型” “方法类型”这个参数设计得很失败, 一个类中的方法,如果方法名和参数个数及类型一样,这个类能正确编译吗? 其实MethodHandle最终只需要暴露类似这样一个静态方法即可: //方法如果没有返回值,为void MethodHandle.<T>invoke( Class clazz, // 方法所属类型(Class) String methodName, // 方法名 Object... params//方法参数,可以运行时确定参数个数及类型,定位到具体方法 ) 你的思路被局限在Java“语言”里了。JSR 292的主要服务对象是JVM上的动态语言,而不是Java。如果你了解JVM的spec而不只是Java的spec,你应该能理解Java字节码不是只能通过Java编译器来生成的。你可以把我之前回帖的那段完整引用一次: RednaxelaFX 写道 star022 写道 定位到一个java方法,其实只需要类型(Class),方法名及参数即可。
对,说得一点也没错,所以MethodHandles的API就是这样的: 引用 findStatic( TestMethodHandle1.class, // 方法所属类型(Class) "hello", // 方法名 type // 由参数和返回值类型组成的“方法类型” ); 如果只是要做Java的method overload resolution,当然只要参数类型不要返回值类型就够了,但了解class文件及JVM内部数据组织方式的话就会知道,方法的签名(signature)在class文件里是以方法描述符(method descriptor)的形式存在,而该描述符上是有返回值类型的。MethodHandles的API这么设计就是为了快,能更直接的访问VM里的信息,以最快的方式找到目标方法。 如果那段文字仍然不能让你明白,那请看下面的例子。 首先要明确的是,在Java语言里,method overload只依赖于方法名和参数类型,不考虑返回值类型;仅在返回值类型不同的方法无法通过Java编译器的编译。 但生成Java字节码的方式有很多:JVM上有非常多其它语言,它们的编译器都可以生成Java字节码;动态代理要生成字节码;再不行,手工生成字节码也是可以的。从JVM的角度看,无论字节码的来源是什么,只要符合class文件规范、只要加载成功,JVM就可以执行那些字节码。 这里我用bitescript来生成一个class文件,类名为TestMethodSameName,包括两个foo方法,它们只在返回值类型上不同: require 'rubygems' require 'bitescript' include BiteScript fb = FileBuilder.build(__FILE__) do public_class 'TestMethodSameName' do public_static_method 'foo', void, int do ldc 'TestMethodSameName.foo:(I)V' aprintln returnvoid end public_static_method 'foo', int, int do ldc 'TestMethodSameName.foo:(I)I' aprintln iload 0 ireturn end public_static_method 'main', void, string[] do push_int 123 invokestatic this, 'foo', [void, int] push_int 456 invokestatic this, 'foo', [int, int] pop returnvoid end end end fb.generate do |filename, class_builder| File.open(filename, 'w') do |file| file.write(class_builder.generate) end end 得到的class文件,内容如下: Compiled from "test5.rb" public class TestMethodSameName extends java.lang.Object{ public static void foo(int); Code: 0: ldc #9; //String TestMethodSameName.foo:(I)V 2: getstatic #15; //Field java/lang/System.out:Ljava/io/PrintStream; 5: swap 6: invokevirtual #21; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V 9: return public static int foo(int); Code: 0: ldc #24; //String TestMethodSameName.foo:(I)I 2: getstatic #15; //Field java/lang/System.out:Ljava/io/PrintStream; 5: swap 6: invokevirtual #21; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V 9: iload_0 10: ireturn public static void main(java.lang.String[]); Code: 0: bipush 123 2: invokestatic #28; //Method foo:(I)V 5: sipush 456 8: invokestatic #30; //Method foo:(I)I 11: pop 12: return } 如果用Java语法来写,就是: public class TestMethodSameName { public static void foo(int i) { System.out.println("TestMethodSameName.foo:(I)V"); } public static int foo(int i) { System.out.println("TestMethodSameName.foo:(I)I"); return i; } public static void main(String[] args) { foo(123); // foo:(I)V foo(456); // foo:(I)I } } 再次注意到这段代码用Java编译器确实编译不了。但上面生成的字节码对JVM来说却是完全没问题的。执行结果输出如下: 引用 TestMethodSameName.foo:(I)V
TestMethodSameName.foo:(I)I 这很好的说明了在深入到底层去挖掘MethodHandle时,指定返回值类型的必要性。 JDK原本包含的普通反射API之所以不需要指定返回值类型是因为它只是为Java语言服务的。如今的JSR 292则是为JVM上所有语言服务,主要目标是各种动态语言,但也不拒绝Java去使用它。 关于bitescript的用法,请参考这一帖的例子。上面生成的class文件我也放在附件里了,不相信例子的输出结果的话请自己执行一下,眼见为实。 |
|
返回顶楼 | |
发表时间:2009-09-28
最后修改:2009-09-28
引用 定位到一个java方法,其实只需要类型(Class),方法名及参数即可。
... MethodHandle版: private static void test(java.dyn.MethodHandle); Signature: (Ljava/dyn/MethodHandle;)V flags: ACC_PRIVATE, ACC_STATIC LineNumberTable: line 7: 0 line 8: 8 line 7: 15 line 10: 21 Code: stack=4, locals=2, args_size=1 0: iconst_0 1: istore_1 2: iload_1 3: ldc #2 // int 100000 5: if_icmpge 21 8: aload_0 9: iconst_1 10: iconst_2 11: iconst_3 12: invokevirtual #3 // Method java/dyn/MethodHandle.invoke:(III)V ... 普通反射版: private static void test(java.lang.reflect.Method) throws java.lang.Throwable; Signature: (Ljava/lang/reflect/Method;)V flags: ACC_PRIVATE, ACC_STATIC LineNumberTable: line 7: 0 line 8: 8 line 7: 39 line 10: 45 Code: stack=6, locals=2, args_size=1 0: iconst_0 1: istore_1 2: iload_1 3: ldc #2 // int 100000 5: if_icmpge 45 8: aload_0 9: aconst_null 10: iconst_3 11: anewarray #3 // class java/lang/Object 14: dup 15: iconst_0 16: iconst_1 17: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 20: aastore 21: dup 22: iconst_1 23: iconst_2 24: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 27: aastore 28: dup 29: iconst_2 30: iconst_3 31: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 34: aastore 35: invokevirtual #5 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; ... 写得很多,看得也累。。。但感觉整个就是答错了方向. 再加上后面回复的人,有好几个在不懂装懂的,整个就一误人子弟的贴。 MethodHandle我还没来得及深究,但汇编,java字节码还是有不少了解的。 如果你认为性能差距的原因是来自: 引用 17: invokestatic #4 // Method java/lang/Integer.valueOf:
这类代码,那么,麻烦你改一改测试类,统一成 private static void doNothing(Integer x, Integer y, Integer z) { } 再比较字节码。 另外, 引用 35: invokevirtual #5 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
这个代码和 引用 12: invokevirtual #3 // Method java/dyn/MethodHandle.invoke:(III)V
相比,就调用本身来说,是没有差别的,唯一差别是在的两个方法的native c的实现上。如果你分析这段c的代码再得出性能差别的原因,才是找对了方向。 另外,如果我没搞错的话,MethodHandle引入返回值查找方法,其根本原因是来自动态语言的返回值与具体类型无关,和性能本身没什么关系。然后, 引用 如果只是要做Java的method overload resolution,当然只要参数类型不要返回值类型就够了,但了解class文件及JVM内部数据组织方式的话就会知道,方法的签名(signature)在class文件里是以方法描述符(method descriptor)的形式存在,而该描述符上是有返回值类型的。MethodHandles的API这么设计就是为了快,能更直接的访问VM里的信息,以最快的方式找到目标方法。
这个只是你自已的推论吧。这个就如同这么一个sql的比方:原先是查询是id=1, 你说,不够快,id=1&retType=2才更快。 |
|
返回顶楼 | |