论坛首页 Java企业应用论坛

通过代码简单介绍JDK 7的MethodHandle,并与.NET的委托对比

浏览 20454 次
该帖已经被评为精华帖
作者 正文
   发表时间:2009-09-27  
定位到一个java方法,其实只需要类型(Class),方法名及参数即可。
0 请登录后投票
   发表时间:2009-09-27  
Saito 写道
treblesoftware 写道
怎么 怎么 怎么 ,第一个例子怎么越看越像反射。

童鞋是不是再仔细看看整篇文章? .. 

    btw: 老赵也就是看见Twitter来一下. 不看见Twitter就窝在blogcn码字..多无趣啊..


其实,我只是调解一下紧张的气氛,才这么说的。
0 请登录后投票
   发表时间:2009-09-27  
反射和MethodHandle差10倍,那MethodHandle和直接的调用差多少?
0 请登录后投票
   发表时间: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功能,所以其开销比接口方法调用还是大很多。不过有两个工程师已经在努力实现相关功能了,可以期待以后的性能改善。
9 请登录后投票
   发表时间:2009-09-28  
没有jsr292的时候,用beanshell不是可以很好的模拟对于动态语言的支持么?
0 请登录后投票
   发表时间:2009-09-28  
还是函数指针好
什么时候java把指针也放出来得了
0 请登录后投票
   发表时间:2009-09-28  
unsid 写道
没有jsr292的时候,用beanshell不是可以很好的模拟对于动态语言的支持么?

BeanShell是JVM上的“一种”动态脚本语言。JRuby、Jython、Rhino这些语言的实现不太可能依靠BeanShell的动态能力去解决。JSR 292就是为了让这些语言更容易实现,实现出来更高效而设计的。我在顶楼的帖的最后留了篇Charles Nutter讲在invokedynamic出现之前实现JRuby的一些麻烦的地方,可以读一下。需要翻墙,这个请自行解决~
0 请登录后投票
   发表时间: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//方法参数,可以运行时确定参数个数及类型,定位到具体方法
)

0 请登录后投票
   发表时间: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文件我也放在附件里了,不相信例子的输出结果的话请自己执行一下,眼见为实。
0 请登录后投票
   发表时间: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才更快。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics