在描述非虚方法调用的内部时,Richter是错误的吗?

时间:2011-02-05 20:11:24

标签: .net clr jit

我会直接把这个问题写给杰弗里里希特,但上次他没有回答我:)所以我会尽力在你的帮助下得到答案,伙计们:)

在第10页的第9版“CLR via C#”一书中,Jeffrey写道:

void M3() {
  Employee e;
  e = new Manager();
  year = e.GetYearsEmployed();
  ...
}
  

M3调用中的下一行代码   员工的非虚拟实例   GetYearsEmployed方法。打电话的时候   一个非虚拟实例方法,即JIT   编译器找到那个类型对象   对应于的类型   用于拨打电话的变量。   在这种情况下,变量e是   定义为员工。 (如果   员工类型没有定义方法   被调用,JIT编译器走了   将类层次结构向下移向Object   寻找这种方法。它可以做到   这是因为每个类型对象都有一个   其中的字段指的是它的基础   类型;此信息未显示在   数字。)然后,JIT编译器   在类型对象中查找条目   方法表引用该方法   被调用,JITs方法(如果   必要的),然后调用JITted   代码。

当我第一次读到这篇文章时,我认为在JIT-ting期间沿着类层次结构寻找方法是没有效果的。在编译阶段很容易找到该方法。但我相信杰弗里。我在另一个论坛上发布了这个信息,另一个人证实了我的疑问,这是奇怪的,并且会无效,而且似乎是错误的信息。

实际上,如果你在反编译器中查找相应的IL代码,例如ILDasm或Reflector(我已经检入过),你会看到IL有一个callvirt指令从基类调用方法,所以JIT不需要查看方法在运行时所在的类:

public class EmployeeBase
{
    public int GetYearsEmployed() { return 1; }
}

public class Employee : EmployeeBase
{
    public void SomeOtherMethod() { }
}

public class Manager : Employee
{
    public void GenProgressReport() { }
}

...

Employee e;
e = new Manager();
int years = e.GetYearsEmployed();

产生的IL是:

L_0000: nop 
L_0001: newobj instance void TestProj.Form1/Manager::.ctor()
L_0006: stloc.0 
L_0007: ldloc.0 
L_0008: callvirt instance int32 TestProj.Form1/EmployeeBase::GetYearsEmployed()
你知道吗?编译器已经发现该方法不在Employee类中,而是在EmployeeBase类中,并且发出了正确的调用。但是从Richter的话来说,JIT必须发现该方法实际上是在运行时位于EmployeeBase类中。

杰弗里里希特错了吗?或者我不明白?

3 个答案:

答案 0 :(得分:2)

C#编译器完全解析非虚拟方法,没有摆动空间。如果在编译调用者之后派生的非虚方法具有相同的签名,则CLR仍将调用C#编译器选择的“固定”方法。这是为了避免脆弱的基类问题。

如果您想要动态方法解析,请使用virtual。如果您不使用virtual,则会获得完全静态分辨率。你的选择。成为this指针的对象引用的运行时类型在非虚拟方法的解析中无关紧要(csc.exe都不适用于CLR JIT)。

JIT将始终调用精确选择的方法。如果方法不存在,它将抛出异常(可能是因为被调用者DLL已被更改)。它不会使用不同的方法。

callvirt也可以调用非虚方法。它用于执行空检查。它以这种方式定义,并且定义C#以对每次调用执行空检查。

答案 1 :(得分:0)

根据我的理解,并使用您的示例: 引擎盖下:

基类中的VIRTUAL方法将在派生类方法表中具有条目。这意味着'object'类型中的所有虚方法都可以在其所有派生类方法表中使用。

NON虚方法(如示例代码中所示),派生类中没有提供的功能实际上在派生类方法表中实际上没有条目!

为了检查这一点,我在WinDbg中运行代码来检查 Manager 类的方法表。

MethodDesc表条目方法De JIT名称

506a4960 503a6728 PreJIT System.Object.ToString()

50698790 503a6730 PreJIT System.Object.Equals(System.Object)

50698360 503a6750 PreJIT System.Object.GetHashCode()

506916f0 503a6764 PreJIT System.Object.Finalize()

001b00c8 00143904 JIT Manager..ctor()

0014c065 001438f8 NONE Manager.GenProgressReport()

所以,我可以看到对象的虚拟对象方法,但我看不到实际的GetYearsEmployed方法,因为它不是虚拟的,也没有派生的实现。 顺便说一句,通过相同的概念,您无法在派生类中看到SomeOtherMethod函数。

但是,您可以调用这些函数,只是它们不在方法表中。我可能是不正确的,但我相信调用堆栈是为了找到它们。也许这就是里希特先生在他的书中的意思。我发现他的书很难读,但那是因为概念很复杂而且比我聪明:)

我不确定IL会反映出这个问题。我相信它可能是IL下面的一层,这就是为什么我用Windbg来看一看。我想你可以使用windbg来看看它在堆栈中行走......

答案 2 :(得分:0)

正如@usr在类似问题中所回答的那样,我发布了How is non-virtual instance method inheritance resolved?

  

运行时通常表示“代码运行的时间/时间”。 JIT   此处的分辨率仅在代码运行之前涉及一次。什么的   JIT没有被称为“在运行时”。

也用杰弗里的话来说

  

JIT编译器定位与其类型对应的类型对​​象   用于拨打电话的变量。

此处的变量类型我认为是“由元数据标记指定的类”(ECMA 335 III.3.19调用),基于JIT解析方法目的地。

C#编译器总是找出正确的调用方法,并将该信息放入元数据标记中。所以JIT永远不必“走下阶级层次”。 (但如果您手动将元数据标记更改为继承的方法,则可以使用

    class A
    {
        public static void Foo() {Console.WriteLine(1); }
        public void Bar() { Console.WriteLine(2); }
    }
    class B : A {}
    class C : B {}

    static void Main()
    {
        C.Foo();
        new C().Bar(); 
        C x = new C();
        x.Bar();
        Console.ReadKey();
    }

IL_0000:  call       void ConsoleApplication5.Program/A::Foo() // change to B::Foo()
IL_0005:  newobj     instance void ConsoleApplication5.Program/C::.ctor()
IL_000a:  call       instance void ConsoleApplication5.Program/A::Bar() // change to B::Bar()
IL_000f:  newobj     instance void ConsoleApplication5.Program/C::.ctor()
IL_0014:  stloc.0
IL_0015:  ldloc.0
IL_0016:  callvirt   instance void ConsoleApplication5.Program/A::Bar() // change to B::Bar()
IL_001b:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
IL_0020:  pop
IL_0021:  ret

如果我们使用Ildasm + Ilasm将A::Foo()更改为B::Foo(),并将A::Bar()更改为B.Bar(),则应用程序运行正常。