Как C # может разрешать виртуальные универсальные методы, где C ++ не может разрешать методы виртуальных шаблонов?

C ++ не поддерживает методы виртуальных шаблонов. Причина в том, что это изменит vtable всякий раз, когда создается новый экземпляр такого метода (он должен быть добавлен к vtable).

Java, напротив, допускает виртуальные универсальные методы. Здесь также ясно, как это может быть реализовано: универсальные методы Java стираются во время выполнения, поэтому универсальный метод является обычным методом во время выполнения, поэтому никаких изменений 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);
}
}

Как CLR отправляет правильный код JIT при вызове M в функции Test_GenericVCall?

27

Решение

Запуск этого кода и анализ 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();
}
}

Я остановил Program.Test с WinDbg:

.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-формате.

29

Другие решения

Я буду называть C ++ templates и C # обобщают «код шаблона», чтобы иметь общий термин.

Шаблон кода в точке, где он генерирует конкретные потребности кода:

  • Полное описание шаблона (исходный код шаблона или что-то подобное)
  • информация о аргументах шаблона, на которых он создается
  • среда компиляции, достаточно надежная для объединения двух

В C ++ шаблон генерирует конкретный код на уровне модуля компиляции. У нас есть полный компилятор, весь исходный код templateи полная информация о типе template аргумент, поэтому мы встряхиваем и печем.

Традиционные дженерики (не-reified) также генерируют конкретный код в аналогичном месте, но затем они позволяют расширение среды выполнения с новыми типами. Таким образом, стирание типа во время выполнения используется вместо полной информации о типе рассматриваемого типа. Java, очевидно, делает это только для того, чтобы избежать необходимости в новом байт-коде для обобщений (см. Кодировку выше).

Reified generics упаковывает сырой универсальный код в какое-то представление, достаточно сильное для повторного применения универсального кода к новому типу. Во время выполнения C # имеет полную копию компилятора, и добавленный тип также содержит в основном полную информацию о том, из чего он был скомпилирован. Со всеми 3 частями, он может повторно применить шаблон на новый тип.

C ++ не несет в себе компилятор, он не хранит достаточно информации о типах или шаблонах для применения во время выполнения. Были предприняты некоторые попытки отложить создание экземпляра шаблона до времени ссылки в C ++.

Таким образом, ваш виртуальный универсальный метод в конечном итоге компилирует новый метод при передаче нового типа. Во время выполнения.

15

И шаблоны C ++, и шаблоны C # являются функциями, разработанными для реализации парадигмы общего программирования: написание алгоритмов и структур данных, которые не зависят от типа данных, которыми они манипулируют.

Но они работают по-разному.

Обобщения работают над внедрением информации о типе в код для обеспечения доступности во время выполнения. Таким образом, различные алгоритмы / структуры данных знают, какие типы они используют, адаптируя себя. Поскольку информация о типе доступна / доступна во время выполнения, решения этого типа могут быть сделаны во время выполнения и зависят от ввода во время выполнения. Вот почему полиморфизм (тоже решение во время выполнения) и дженерики C # хорошо работают вместе.

Шаблоны C ++, с другой стороны, совсем другой зверь. Это система генерации кода во время компиляции. Это означает, что система шаблонов должна генерировать во время компиляции разные версии кода в зависимости от используемых типов. Даже если это может привести ко многим мощным вещам, которых нет у дженериков (на самом деле система шаблонов C ++ является Turing Complete), генерация кода выполняется во время компиляции, поэтому мы должны знать типы, используемые во время компиляции.
Поскольку шаблоны просто генерируют разные версии кода для разных используемых типов, предоставляется шаблон функции template<typename T> void foo( const T& t );, foo( 1 ) а также foo( 'c' ) не вызывать ту же функцию, они называют int а также char сгенерированные версии соответственно.

Вот почему полиморфизм нельзя использовать вместе с шаблонами: каждый экземпляр шаблона функции имеет свою особую функцию, поэтому придание шаблону полиморфности не имеет смысла. Какую версию следует вызывать во время выполнения?

7

C ++ обычно компилируется прямо в нативный код, а нативный код для C.Foo<int>(int) а также C.Foo<long>(long) может быть другим. Кроме того, C ++ обычно хранит указатели на нативный код в vtable. Объедините их, и вы увидите, что если C.Foo<T> является виртуальным, то указатель на каждый экземпляр должен быть частью vtable.

C # не имеет этой проблемы. C # компилируется в IL, а IL JITted для нативного кода. Таблицы IL не содержат указателей на нативный код, они содержат указатели на IL (вроде). Кроме того, .NET-дженерики не допускают специализаций. Итак, на уровне IL, C.Foo<int>(int) а также C.Foo<long>(long) будут всегда смотреть именно так тот же самый.

Следовательно, проблема C ++ просто не существует для C # и не является проблемой, требующей решения.

П.С .: Подход Java на самом деле используется и средой исполнения .NET. Часто общие методы приводят к точный тот же нативный код, независимо от аргумента универсального типа, и в этом случае будет только один экземпляр этого метода. Вот почему вы иногда видите ссылки на System.__Canon в следах стека и тому подобное, это грубый эквивалент времени выполнения Java ?,

5

Прошло много времени с тех пор, как я делал вещи на 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 ++.

4
По вопросам рекламы [email protected]