как осуществляется вызов виртуального универсального метода? - PullRequest
17 голосов
/ 04 июля 2011

Мне интересно, как в CLR реализованы такие вызовы:

abstract class A {
    public abstract void Foo<T, U, V>();
}

A a = ...
a.Foo<int, string, decimal>(); // <=== ?

Является ли этот вызов причиной своего рода поиска хеш-карты по токенам параметров типа в качестве ключей и скомпилированной специализации универсального метода (одиндля всех ссылочных типов и различного кода для всех типов значений) в качестве значений?

Ответы [ 3 ]

13 голосов
/ 05 июля 2011

Я не нашел много точной информации по этому поводу, поэтому большая часть этого ответа основана на превосходной статье о дженериках .Net 2001 (даже до выхода .Net 1.0!), Один короткийзапишите в последующий документ и то, что я собрал из исходного кода SSCLI v. 2.0 (хотя я не смог найти точный код для вызова виртуальных обобщенных методов).

Давайте начнем с простого: как вызывается неуниверсальный не виртуальный метод?Путем прямого вызова кода метода, чтобы скомпилированный код содержал прямой адрес.Компилятор получает адрес метода из таблицы методов (см. Следующий абзац).Это может быть так просто?Ну, почти.Тот факт, что методы JITed, делает его немного более сложным: на самом деле вызывается либо код, который компилирует метод и только потом выполняет его, если он еще не скомпилирован;или это одна инструкция, которая напрямую вызывает скомпилированный код, если он уже существует.Далее я собираюсь игнорировать эту деталь.

Теперь, как называется неуниверсальный виртуальный метод?Подобно полиморфизму в таких языках, как C ++, существует таблица методов, доступная из указателя this (ссылка).Каждый производный класс имеет свою собственную таблицу методов и свои методы.Итак, чтобы вызвать виртуальный метод, получите ссылку на this (переданную в качестве параметра), оттуда получите ссылку на таблицу методов, посмотрите на правильную запись в ней (номер записи постоянен для конкретной функции) и вызовите код, на который указывает запись.Вызов методов через интерфейсы немного сложнее, но сейчас нам это не интересно.

Теперь нам нужно узнать о совместном использовании кода.Код может быть разделен между двумя «экземплярами» одного и того же метода, если ссылочные типы в параметрах типов соответствуют любым другим ссылочным типам, а типы значений абсолютно одинаковы.Так, например, C<string>.M<int>() делит код с C<object>.M<int>(), но не с C<string>.M<byte>().Нет никакой разницы между параметрами типа типа и параметрами типа метода.(В оригинальной статье 2001 года упоминается, что код может использоваться совместно, если оба параметра struct s с одинаковым макетом, но я не уверен, что это действительно так в реальной реализации.)

Давайте сделаемпромежуточный шаг на пути к универсальным методам: неуниверсальные методы в универсальных типах.Из-за совместного использования кода нам нужно откуда-то получить параметры типа (например, для вызова кода типа new T[]).По этой причине каждый экземпляр универсального типа (например, C<string> и C<object>) имеет свой собственный дескриптор типа, который содержит параметры типа, а также таблицу методов.Обычные методы могут обращаться к этому дескриптору типа (технически это структура, вызывающая путаницу MethodTable, даже если она содержит не только таблицу методов) из ссылки this.Существует два типа методов, которые не могут этого сделать: статические методы и методы для типов значений.Для них дескриптор типа передается как скрытый аргумент.

Для не виртуальных универсальных методов, дескриптор типа не достаточно, и поэтому они получают другой скрытый аргумент, MethodDesc, который содержит параметры типа,Кроме того, компилятор не может хранить экземпляры в таблице обычных методов, потому что это статично.Таким образом, он создает вторую, другую таблицу методов для универсальных методов, которая индексируется по параметрам типа, и получает оттуда адрес метода, если он уже существует с совместимыми параметрами типа, или создает новую запись.

Virtualуниверсальные методы теперь просты: компилятор не знает конкретный тип, поэтому он должен использовать таблицу методов во время выполнения.И обычная таблица методов не может быть использована, поэтому она должна искать в специальной таблице методов общие методы.Конечно, скрытый параметр, содержащий параметры типа, все еще присутствует.

При изучении этого возникла одна интересная деталь: поскольку JITer очень ленив, работает следующий (совершенно бесполезный) код:

object Lift<T>(int count) where T : new()
{
    if (count == 0)
        return new T();

    return Lift<List<T>>(count - 1);
}

Эквивалентный код C ++ заставляет компилятор отказаться от переполнения стека.

4 голосов
/ 04 июля 2011

Да.Код для определенного типа генерируется CLR во время выполнения и сохраняет хэш-таблицу (или аналогичную) реализаций.

Страница 372 из CLR через C # :

Когда метод, использующий параметры универсального типа, JIT-компилируется, CLR принимает IL метода, заменяет аргументы указанного типа, а затем создает собственный код, специфичный для этого метода, работающего с указанными типами данных.,Это именно то, что вы хотите, и является одной из основных особенностей дженериков.Однако в этом есть и обратная сторона: CLR продолжает генерировать собственный код для каждой комбинации метода / типа.Это называется взрывом кода.Это может существенно увеличить рабочий набор приложения, что приведет к снижению производительности.К счастью, в CLR встроены некоторые оптимизации для уменьшения взрыва кода.Во-первых, если метод вызывается для определенного аргумента типа, а затем метод вызывается снова с использованием того же аргумента типа, CLR скомпилирует код для этой комбинации метод / тип только один раз. Так что еслиодна сборка использует List, а совершенно другая сборка (загруженная в тот же AppDomain) также использует List, CLR скомпилирует методы для List всего один раз.Это существенно снижает вероятность взлома кода.

0 голосов
/ 04 ноября 2013

EDIT

Я сейчас сталкивался. Я сейчас сталкивался с https://msdn.microsoft.com/en-us/library/sbh15dya.aspx, в котором четко говорится, что генерики при использовании ссылочных типов повторно используют один и тот же код, поэтому я бы принял это как окончательный авторитет.

ОРИГИНАЛЬНЫЙ ОТВЕТ

Я вижу здесь два несогласных ответа, и у обоих есть ссылки на их сторону, поэтому я попытаюсь добавить свои два цента.

Во-первых, Clr via C # Джеффри Рихтера, опубликованный Microsoft Press, так же действителен, как и блог msdn, тем более, что блог уже устарел (для получения дополнительных книг смотрите «1015 ** 1016») что он эксперт по windows и .net).

Теперь позвольте мне сделать мой собственный анализ.

Очевидно, что два универсальных типа, которые содержат разные аргументы ссылочного типа, не могут использовать один и тот же код

Например, List и List > не могут совместно использовать один и тот же код, так как это приведет к возможности добавить объект TypeA в List с помощью отражения, и clr будет строго типизирован для генетики. также (в отличие от Java, в котором только компилятор проверяет generic, но базовая JVM не имеет понятия о них).

И это относится не только к типам, но и к методам, поскольку, например, универсальный метод типа T может создать объект типа T (например, ничто не мешает ему создать новый List ), в этом случае повторное использование одного и того же кода может привести к хаосу.

Кроме того, метод GetType не может быть переопределен, и он фактически всегда возвращает правильный универсальный тип, доказывая, что каждый аргумент типа действительно имеет свой собственный код. (Этот пункт даже важнее, чем кажется, поскольку clr и jit работают на основе объекта типа, созданного для этого объекта, с помощью GetType (), что просто означает, что для каждого аргумента типа должен быть отдельный объект, даже для ссылочных типов )

Еще одна проблема, которая может возникнуть в результате повторного использования кода, поскольку операторы is и as больше не будут работать правильно, и в целом у всех типов приведения будут серьезные проблемы.

СЕЙЧАС ДЛЯ АКТУАЛЬНОЙ ТЕСТИРОВАНИЯ:

Я проверил его, имея универсальный тип, который содержал статический член, и затем создал два объекта с различными параметрами типа, и статические поля явно не были разделены, ясно доказывая, что код не является общим даже для ссылочных типов.

EDIT:

См. http://blogs.msdn.com/b/csharpfaq/archive/2004/03/12/how-do-c-generics-compare-to-c-templates.aspx о том, как это реализовано:

Использование пространства

Использование пробела в C ++ и C # различно. Потому что C ++ шаблоны создаются во время компиляции, каждое использование различного типа в шаблон приводит к тому, что отдельный фрагмент кода создается компилятор.

В мире C # все несколько иначе. Фактические реализации с использованием определенного типа создаются во время выполнения. Когда среда выполнения создает типа как List, JIT увидит, если это уже было создано. Если это так, он просто использует этот код. Если нет, это займет IL, который сгенерировал компилятор и делает соответствующие замены с фактическим типом.

Это не совсем правильно. Существует отдельный собственный путь к коду каждый тип значения, но так как все ссылочные типы имеют размер ссылки, они могут поделиться своей реализацией.

Это означает, что подход C # должен иметь меньшую площадь диск, и в памяти, так что это преимущество для дженериков по сравнению с C ++ шаблоны.

Фактически, компоновщик C ++ реализует функцию, известную как «шаблон». сворачивание », где компоновщик ищет разделы собственного кода, которые идентичны, и если он их находит, складывает их вместе. Так что это не четкий, как это может показаться.

Как можно видеть, CLR «может» повторно использовать реализацию для ссылочных типов, как это делают текущие компиляторы c ++, однако на это нет никаких гарантий, и для небезопасного кода, использующего stackalloc и указатели, это, вероятно, не так, и могут быть и другие ситуации.

Однако мы должны знать, что в системе типов CLR они обрабатываются как разные типы, такие как разные вызовы статических конструкторов, отдельные статические поля, отдельные объекты типа и объект с аргументом типа T1, не должны быть в состоянии получить доступ к приватному полю другого объекта с аргументом типа T2 (хотя для объекта того же типа действительно возможно получить доступ к приватным полям из другого объекта того же типа).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...