如何C#允许虚拟通用方法,C不能允许虚拟模板方法?

C不支持虚拟模板方法。原因是这将改变vtable每当这样的方法的新实例化(它必须添加到vtable)。

相比之下,Java允许虚拟通用方法。这里,它也清楚如何实现:Java泛型在运行时被擦除,所以一个通用的方法是一个通常的方法在运行时,所以没有更改的vtable必要的。

但现在到C#。 C#确实有泛型。使用reified泛型,特别是当使用值类型作为类型参数时,必须有一个通用方法的不同版本。但是,我们有同样的问题,因为C有:我们需要改变vtable每当一个通用方法的新实例化。

我不太深入C#的内部工作,所以我的直觉可能完全错了。因此,有人能更深入地了解C#/。NET告诉我他们如何能够在C#中实现通用虚拟方法?

这里的代码显示我的意思:

[MethodImpl(MethodImplOptions.NoInlining)]
static void Test_GenericVCall()
{
    var b = GetA();
    b.M<string>();
    b.M<int>();
}

[MethodImpl(MethodImplOptions.NoInlining)]
static A GetA()
{
    return new B();
}

class A
{
    public virtual void M<T>()
    {
    }
}

class B : A
{
    public override void M<T>()
    {
        base.M<T>();
        Console.WriteLine(typeof(T).Name);
    }
}

当在函数Test_GenericVCall中调用M时,CLR如何调度到正确的JIT代码?

最佳答案
运行此代码并分析IL和生成的ASM允许我们看到发生了什么:

internal class Program
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void Test()
    {
        var b = GetA();
        b.GenericVirtual<string>();
        b.GenericVirtual<int>();
        b.GenericVirtual<StringBuilder>();
        b.GenericVirtual<int>();
        b.GenericVirtual<StringBuilder>();
        b.GenericVirtual<string>();
        b.NormalVirtual();
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static A GetA()
    {
        return new B();
    }

    private class A
    {
        public virtual void GenericVirtual<T>()
        {
        }

        public virtual void NormalVirtual()
        {
        }
    }

    private class B : A
    {
        public override void GenericVirtual<T>()
        {
            base.GenericVirtual<T>();
            Console.WriteLine("Generic virtual: {0}", typeof(T).Name);
        }

        public override void NormalVirtual()
        {
            base.NormalVirtual();
            Console.WriteLine("Normal virtual");
        }
    }

    public static void Main(string[] args)
    {
        Test();
        Console.ReadLine();
        Test();
    }
}

我断点与WinDbg的Program.Test:

.loadby sos clr; !bpmd CSharpNewTest CSharpNewTest.Program.Test

然后我使用Sosex.dll‘s great!muf命令显示我交错的源,IL和ASM:

0:000> !muf
CSharpNewTest.Program.Test(): void
    b:A

        002e0080 55              push    ebp
        002e0081 8bec            mov     ebp,esp
        002e0083 56              push    esi
var b = GetA();
    IL_0000: call CSharpNewTest.Program::GetA()
    IL_0005: stloc.0  (b)
>>>>>>>>002e0084 ff15c0371800    call    dword ptr ds:[1837C0h]
        002e008a 8bf0            mov     esi,eax
b.GenericVirtual<string>();
    IL_0006: ldloc.0  (b)
    IL_0007: callvirt A::GenericVirtuallong
        002e008c 6800391800      push    183900h
        002e0091 8bce            mov     ecx,esi
        002e0093 ba50381800      mov     edx,183850h
        002e0098 e877e49b71      call    clr!JIT_VirtualFunctionPointer (71c9e514)
        002e009d 8bce            mov     ecx,esi
        002e009f ffd0            call    eax
b.GenericVirtual<int>();
    IL_000c: ldloc.0  (b)
    IL_000d: callvirt A::GenericVirtuallong
        002e00a1 6830391800      push    183930h
        002e00a6 8bce            mov     ecx,esi
        002e00a8 ba50381800      mov     edx,183850h
        002e00ad e862e49b71      call    clr!JIT_VirtualFunctionPointer (71c9e514)
        002e00b2 8bce            mov     ecx,esi
        002e00b4 ffd0            call    eax
b.GenericVirtual<StringBuilder>();
    IL_0012: ldloc.0  (b)
    IL_0013: callvirt A::GenericVirtuallong
        002e00b6 6870391800      push    183970h
        002e00bb 8bce            mov     ecx,esi
        002e00bd ba50381800      mov     edx,183850h
        002e00c2 e84de49b71      call    clr!JIT_VirtualFunctionPointer (71c9e514)
        002e00c7 8bce            mov     ecx,esi
        002e00c9 ffd0            call    eax
b.GenericVirtual<int>();
    IL_0018: ldloc.0  (b)
    IL_0019: callvirt A::GenericVirtuallong
        002e00cb 6830391800      push    183930h
        002e00d0 8bce            mov     ecx,esi
        002e00d2 ba50381800      mov     edx,183850h
        002e00d7 e838e49b71      call    clr!JIT_VirtualFunctionPointer (71c9e514)
        002e00dc 8bce            mov     ecx,esi
        002e00de ffd0            call    eax
b.GenericVirtual<StringBuilder>();
    IL_001e: ldloc.0  (b)
    IL_001f: callvirt A::GenericVirtuallong
        002e00e0 6870391800      push    183970h
        002e00e5 8bce            mov     ecx,esi
        002e00e7 ba50381800      mov     edx,183850h
        002e00ec e823e49b71      call    clr!JIT_VirtualFunctionPointer (71c9e514)
        002e00f1 8bce            mov     ecx,esi
        002e00f3 ffd0            call    eax
b.GenericVirtual<string>();
    IL_0024: ldloc.0  (b)
    IL_0025: callvirt A::GenericVirtuallong
        002e00f5 6800391800      push    183900h
        002e00fa 8bce            mov     ecx,esi
        002e00fc ba50381800      mov     edx,183850h
        002e0101 e80ee49b71      call    clr!JIT_VirtualFunctionPointer (71c9e514)
        002e0106 8bce            mov     ecx,esi
        002e0108 ffd0            call    eax
b.NormalVirtual();
    IL_002a: ldloc.0  (b)
        002e010a 8bce            mov     ecx,esi
        002e010c 8b01            mov     eax,dword ptr [ecx]
        002e010e 8b4028          mov     eax,dword ptr [eax+28h]
    IL_002b: callvirt A::NormalVirtual()
        002e0111 ff5014          call    dword ptr [eax+14h]
}
    IL_0030: ret 

感兴趣的是正常的虚拟呼叫,可以与通用虚拟呼叫进行比较:

b.NormalVirtual();
    IL_002a: ldloc.0  (b)
        002e010a 8bce            mov     ecx,esi
        002e010c 8b01            mov     eax,dword ptr [ecx]
        002e010e 8b4028          mov     eax,dword ptr [eax+28h]
    IL_002b: callvirt A::NormalVirtual()
        002e0111 ff5014          call    dword ptr [eax+14h]

看起来很标准。让我们来看看通用调用:

b.GenericVirtual<string>();
    IL_0024: ldloc.0  (b)
    IL_0025: callvirt A::GenericVirtuallong
        002e00f5 6800391800      push    183900h
        002e00fa 8bce            mov     ecx,esi
        002e00fc ba50381800      mov     edx,183850h
        002e0101 e80ee49b71      call    clr!JIT_VirtualFunctionPointer (71c9e514)
        002e0106 8bce            mov     ecx,esi
        002e0108 ffd0            call    eax

好,所以通用的虚拟调用通过加载我们的对象b(在esi,被移动到ecx),然后调用clr!JIT_VirtualFunctionPointer来处理。两个常数也推送:183850在edx。我们可以得出结论,这可能是用于函数A.GenericVirtual< T>的句柄,因为它对于6个呼叫站点中的任何一个都不改变。
另一个常数,183900,看起来是通用参数的类型句柄。
事实上,SSCLI证实了怀疑:

HCIMPL3(CORINFO_MethodPtr,JIT_VirtualFunctionPointer,Object * objectUNSA​​FE,
                                                       CORINFO_CLASS_HANDLE classHnd,
                                                       CORINFO_METHOD_HANDLE方法结束)

因此,查找基本上委托给JIT_VirtualFunctionPointer,它必须准备一个可以调用的地址。假设它将JIT它和返回一个指针到JIT的代码,或做一个蹦床,当第一次调用,将JIT的功能。

0:000> uf clr!JIT_VirtualFunctionPointer
clr!JIT_VirtualFunctionPointer:
71c9e514 55              push    ebp
71c9e515 8bec            mov     ebp,esp
71c9e517 83e4f8          and     esp,0FFFFFFF8h
71c9e51a 83ec0c          sub     esp,0Ch
71c9e51d 53              push    ebx
71c9e51e 56              push    esi
71c9e51f 8bf2            mov     esi,edx
71c9e521 8bd1            mov     edx,ecx
71c9e523 57              push    edi
71c9e524 89542414        mov     dword ptr [esp+14h],edx
71c9e528 8b7d08          mov     edi,dword ptr [ebp+8]
71c9e52b 85d2            test    edx,edx
71c9e52d 745c            je      clr!JIT_VirtualFunctionPointer+0x70 (71c9e58b)

clr!JIT_VirtualFunctionPointer+0x1b:
71c9e52f 8b12            mov     edx,dword ptr [edx]
71c9e531 89542410        mov     dword ptr [esp+10h],edx
71c9e535 8bce            mov     ecx,esi
71c9e537 c1c105          rol     ecx,5
71c9e53a 8bdf            mov     ebx,edi
71c9e53c 03ca            add     ecx,edx
71c9e53e c1cb05          ror     ebx,5
71c9e541 03d9            add     ebx,ecx
71c9e543 a180832872      mov     eax,dword ptr [clr!g_pJitGenericHandleCache (72288380)]
71c9e548 8b4810          mov     ecx,dword ptr [eax+10h]
71c9e54b 33d2            xor     edx,edx
71c9e54d 8bc3            mov     eax,ebx
71c9e54f f77104          div     eax,dword ptr [ecx+4]
71c9e552 8b01            mov     eax,dword ptr [ecx]
71c9e554 8b0490          mov     eax,dword ptr [eax+edx*4]
71c9e557 85c0            test    eax,eax
71c9e559 7430            je      clr!JIT_VirtualFunctionPointer+0x70 (71c9e58b)

clr!JIT_VirtualFunctionPointer+0x47:
71c9e55b 8b4c2410        mov     ecx,dword ptr [esp+10h]

clr!JIT_VirtualFunctionPointer+0x50:
71c9e55f 395804          cmp     dword ptr [eax+4],ebx
71c9e562 7521            jne     clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)

clr!JIT_VirtualFunctionPointer+0x55:
71c9e564 39480c          cmp     dword ptr [eax+0Ch],ecx
71c9e567 751c            jne     clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)

clr!JIT_VirtualFunctionPointer+0x5a:
71c9e569 397010          cmp     dword ptr [eax+10h],esi
71c9e56c 7517            jne     clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)

clr!JIT_VirtualFunctionPointer+0x5f:
71c9e56e 397814          cmp     dword ptr [eax+14h],edi
71c9e571 7512            jne     clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)

clr!JIT_VirtualFunctionPointer+0x64:
71c9e573 f6401801        test    byte ptr [eax+18h],1
71c9e577 740c            je      clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)

clr!JIT_VirtualFunctionPointer+0x85:
71c9e579 8b4008          mov     eax,dword ptr [eax+8]
71c9e57c 5f              pop     edi
71c9e57d 5e              pop     esi
71c9e57e 5b              pop     ebx
71c9e57f 8be5            mov     esp,ebp
71c9e581 5d              pop     ebp
71c9e582 c20400          ret     4

clr!JIT_VirtualFunctionPointer+0x6a:
71c9e585 8b00            mov     eax,dword ptr [eax]
71c9e587 85c0            test    eax,eax
71c9e589 75d4            jne     clr!JIT_VirtualFunctionPointer+0x50 (71c9e55f)

clr!JIT_VirtualFunctionPointer+0x70:
71c9e58b 8b4c2414        mov     ecx,dword ptr [esp+14h]
71c9e58f 57              push    edi
71c9e590 8bd6            mov     edx,esi
71c9e592 e8c4800400      call    clr!JIT_VirtualFunctionPointer_Framed (71ce665b)
71c9e597 5f              pop     edi
71c9e598 5e              pop     esi
71c9e599 5b              pop     ebx
71c9e59a 8be5            mov     esp,ebp
71c9e59c 5d              pop     ebp
71c9e59d c20400          ret     4

实现可以在SSCLI中查看,它似乎仍然适用:

HCIMPL3(CORINFO_MethodPtr, JIT_VirtualFunctionPointer, Object * objectUNSAFE,
                                                       CORINFO_CLASS_HANDLE classHnd,
                                                       CORINFO_METHOD_HANDLE methodHnd)
{
    CONTRACTL {
        SO_TOLERANT;
        THROWS;
        DISABLED(GC_TRIGGERS);      // currently disabled because of FORBIDGC in HCIMPL
    } CONTRACTL_END;

    OBJECTREF objRef = ObjectToOBJECTREF(objectUNSAFE);

    if (objRef != NULL && g_pJitGenericHandleCache)
    {
        JitGenericHandleCacheKey key(objRef->GetMethodTable(), classHnd, methodHnd);
        HashDatum res;
        if (g_pJitGenericHandleCache->GetValueSpeculative(&key,&res))
            return (CORINFO_GENERIC_HANDLE)res;
    }

    // Tailcall to the slow helper
    ENDFORBIDGC();
    return HCCALL3(JIT_VirtualFunctionPointer_Framed, OBJECTREFToObject(objRef), classHnd, methodHnd);
}
HCIMPLEND

所以基本上它检查一个缓存,看看我们是否已经看到这个类型/类组合之前,否则发送到JIT_VirtualFunctionPointer_Framed,它调用MethodDesc :: GetMultiCallableAddrOfVirtualizedCode获取它的地址。 MethodDesc调用传递对象引用和通用类型句柄,因此它可以查找要分发到什么虚拟函数,以及什么版本的虚函数(即具有什么通用参数)。

所有这一切都可以在SSCLI中查看,如果你想更深入地 – 似乎这没有改变与4.0版本的CLR。

总之,CLR做你期望的;生成携带调用虚拟通用功能的类型的信息的不同呼叫站点。然后将其传递给CLR以进行调度。复杂性是,CLR必须跟踪通用虚拟函数及其已经被插入的版本。

转载注明原文:如何C#允许虚拟通用方法,C不能允许虚拟模板方法? - 代码日志