代码改变世界

CLR 虚方法调用和接口方法调用

2013-08-05 22:49  Dirichlet  阅读(326)  评论(0)    收藏  举报
不知接口方法和虚方法分发有什么区别?似乎在CIL中都是callvirt指令。

对,MSIL里都是callvirt,但JIT的时候得到了不同的处理:
对虚方法的分发是编译成这样:

X86代码 复制代码
  1. mov  ecx, esi              ; 假设现在ESI是一个指向对象实例的指针,复制到ECX里 
  2. mov  eax, dword ptr [ecx]  ; 对象实例的第一项是指向方法表的指针,复制到EAX里 
  3. call dword ptr [eax + 7Ch] ; EAX现在指向方法表,0x7C是我随便写的一个偏移量。这个在加载类的时候就可以确定 
mov  ecx, esi              ; 假设现在ESI是一个指向对象实例的指针,复制到ECX里
mov  eax, dword ptr [ecx]  ; 对象实例的第一项是指向方法表的指针,复制到EAX里
call dword ptr [eax + 7Ch] ; EAX现在指向方法表,0x7C是我随便写的一个偏移量。这个在加载类的时候就可以确定

对接口方法的分发是编译成:

X86代码 复制代码
  1. mov  ecx, esi             ; 跟上面一样,ESI是指向对象的指针,复制到ECX 
  2. call dword ptr [0099EEA0h] ; JIT的时候指向一个固定地址的stub(这里数字是随便编的,别在意) 
mov  ecx, esi             ; 跟上面一样,ESI是指向对象的指针,复制到ECX
call dword ptr [0099EEA0h] ; JIT的时候指向一个固定地址的stub(这里数字是随便编的,别在意)

这个call是对一个固定地址做间接调用。一开始是调用到一个通用的resolver stub,去找具体的方法的地址。如果在同一个调用点出现两次相同类型的被调用对象,则间接调用会指向为该类型特化的一个dispatch stub上,样子类似这样:

X86代码 复制代码
  1. cmp dword ptr [ecx], 009A3377h ; 后面的常量是特定类型的方法表地址 
  2. jne 0091A012                   ; 比较不相等的话,跳转到一个特定的resolver stub上 
  3. jmp 00EBD070                   ; 相等的话则直接跳转到目标方法的地址 
cmp dword ptr [ecx], 009A3377h ; 后面的常量是特定类型的方法表地址
jne 0091A012                   ; 比较不相等的话,跳转到一个特定的resolver stub上
jmp 00EBD070                   ; 相等的话则直接跳转到目标方法的地址

这段代码被称为monomorphic inline method cache,怎么翻译好呢……单态内联方法缓存? 比较失败时会跳转到一个resolver stub;它会维护一个“不命中计数器”。如果在某个调用点累计失败了100次,就会再次更新之前的间接调用为直接指向通用的resolver stub。
这样,如果在一个接口方法的调用点上总是同一个类型的实例被调用,则分发效率跟虚方法调用差不多快。如果被调用对象的类型经常变,速度就会慢下来了……