C ++不支持虚拟模板方法。原因是只要进行了这种方法的新实例化(必须将其添加到vtable
),这将改变vtable
。
vtable
。
但现在转向C#。 C#确实有具体的泛型。使用具体化的泛型,特别是当使用值类型作为类型参数时,必须有不同版本的泛型方法。但是后来我们遇到了和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);
}
}
在函数M
中调用Test_GenericVCall
时,CLR如何调度到正确的JITed代码?
答案 0 :(得分:30)
运行此代码并分析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的精彩!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 * objectUNSAFE,
CORINFO_CLASS_HANDLE classHnd,
CORINFO_METHOD_HANDLE methodHnd)
因此,查找基本上委托给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中查看 - 似乎这与CLR的4.0版本没有改变。
简而言之,CLR可以满足您的期望;生成不同的呼叫站点,这些站点携带调用虚拟通用功能的类型的信息。然后将其传递给CLR以进行调度。复杂性是CLR必须跟踪通用虚拟功能及其具有JIT的版本。
答案 1 :(得分:16)
我将调用C ++ template
和C#generics'模式代码'以便有一个共同的术语。
模式代码在生成具体代码时需要:
在C ++中,模式在编译单元级别生成具体代码。我们有完整的编译器,template
的完整源代码,以及template
参数的完整类型信息,所以我们摇动和烘烤。
传统泛型(非定义)也会在类似的位置生成具体代码,但它们允许使用新类型进行运行时扩展。因此,使用运行时类型擦除而不是所讨论类型的完整类型信息。 Java显然只是为了避免需要新的字节码用于泛型(参见上面的编码)。
Reified泛型将原始通用代码打包成某种强大的表示形式,以便在新类型上重新应用泛型。在运行时,C#具有编译器的完整副本,并且添加的类型也基本上包含有关它的编译内容的完整信息。通过全部3个部分,它可以在新类型上重新应用模式。
C ++不带编译器,它不存储有关在运行时应用的类型或模板的足够信息。已经尝试将模板实例化延迟到C ++中的链接时间。
因此,当传递新类型时,您的虚拟泛型方法最终会编译新方法。在运行时。
答案 2 :(得分:7)
C ++模板和C#泛型都是为实现泛型编程范式而设计的功能:编写算法和数据结构,它们并不依赖于它们操作的数据类型。
但他们的工作方式非常不同。
泛型工作注入有关代码的类型信息,以便在运行时可用。因此,不同的算法/数据结构知道他们正在使用什么类型,适应自己。由于类型信息在运行时是可用的/可访问的,因此类型的decissions可以在运行时完成并依赖于运行时输入。这就是为什么多态(A运行时间衰减)和C#泛型可以很好地协同工作。
另一方面,C ++模板是一个非常不同的野兽。它们是编译时代码生成系统。这意味着模板系统所做的是在编译时根据所使用的类型生成不同版本的代码。即使这可以实现泛型不具备的许多强大功能(实际上C ++模板系统是Turing Complete),代码生成也是在编译时完成的,所以我们必须知道编译时使用的类型-time 即可。template<typename T> void foo( const T& t );
,foo( 1 )
和foo( 'c' )
不要调用相同的函数,他们分别调用int
和char
生成的版本。
这就是为什么多态不能与模板一起使用:每个函数模板实例都是一个独特的函数,因此使模板具有多态性是没有意义的。什么版本应该在运行时调用?。
答案 3 :(得分:5)
C ++通常直接编译为本机代码,C.Foo<int>(int)
和C.Foo<long>(long)
的本机代码可能不同。此外,C ++通常在vtable中存储指向本机代码的指针。结合这些,你会发现如果C.Foo<T>
是虚拟的,那么指向每个实例化的指针都需要成为vtable的一部分。
C#没有这个问题。 C#编译为IL,IL被JIT编译为本机代码。 IL vtables不包含指向本机代码的指针,它们包含指向IL的指针(有点)。除此之外,.NET泛型不允许专业化。因此,在IL级别,C.Foo<int>(int)
和C.Foo<long>(long)
始终 。
因此,C ++对C#的问题根本不存在,并且不是一个需要解决的问题。
P.S。:Java方法实际上也是由.NET运行时使用的。通常,泛型方法将导致 exact 相同的本机代码,无论泛型类型参数如何,在这种情况下,只有该方法的一个实例。这就是为什么有时会在堆栈跟踪中看到对System.__Canon
的引用的原因,它等同于Java ?
的粗略运行时。
答案 4 :(得分:4)
自从我在C#泛型之前做C#之后已经很长时间了,所以我不知道C#实现通常如何在内部做事。
但是,在C ++方面,虚拟模板受到单独翻译每个翻译单元的设计目标的限制。
以下是虚拟函数模板的假设示例,它不会使用当前的C ++进行编译:
#include <iostream>
using namespace std;
struct Base
{
template< int n >
virtual void foo() { cout << "Base::foo<" << n << ">" << endl; }
static auto instance() -> Base&;
};
auto main()
-> int
{
Base::instance().foo<666>();
}
//-------------------------------- Elsewhere:
struct Derived: Base
{
template< int n >
virtual void foo() { cout << "Derived::foo<" << n << ">" << endl; }
};
auto Base::instance() -> Base&
{
static Derived o;
return o;
}
以下是如何手动实施的:
#include <iostream>
#include <map>
#include <typeindex>
using namespace std;
struct Base
{
virtual ~Base() {}
template< int n >
struct foo_pointer
{
void (*p)( Base* );
};
template< int n >
using Foo_pointer_map = map<type_index, foo_pointer< n >>;
template< int n >
static
auto foo_pointer_map()
-> Foo_pointer_map< n >&
{
static Foo_pointer_map< n > the_map;
return the_map;
}
template< int n >
static
void foo_impl( Base* ) { cout << "Base::foo<" << n << ">" << endl; }
template< int n >
void foo() { foo_pointer_map< n >()[type_index( typeid( *this ) )].p( this ); }
static auto instance() -> Base&;
};
bool const init_Base = []() -> bool
{
Base::foo_pointer_map<666>()[type_index( typeid( Base ) )].p = &Base::foo_impl<666>;
return true;
}();
auto main()
-> int
{
Base::instance().foo<666>();
}
//-------------------------------- Elsewhere:
struct Derived: Base
{
template< int n >
static
void foo_impl( Base* ) { cout << "Derived::foo<" << n << ">" << endl; }
};
bool const init_Derived = []() -> bool
{
// Here one must know about the instantiation of the base class function with n=666.
Base::foo_pointer_map<666>()[type_index( typeid( Derived ) )].p = &Derived::foo_impl<666>;
return true;
}();
auto Base::instance() -> Base&
{
static Derived o;
return o;
}
此代码编译并生成第一个代码所期望的结果,但仅通过使用有关模板的所有实例化的知识,可能位于不同翻译单元的实例化。
在初始化查找表时,通常无法获得这些知识。
尽管如此,现代C ++编译器确实提供了整个程序优化,并且可能在链接时生成代码,因此它可能不会超越当前的技术。即不是技术上的不可能性,而是不切实际的。除此之外还有动态库的问题,当然C ++不支持,但仍然是C ++编程实际现实的一部分。