Реализация CLR вызовов виртуальных методов для членов интерфейса

Из любопытства: как CLR отправляет вызовы виртуального метода членам интерфейса для правильной реализации?

Я знаю о VTable, который CLR поддерживает для каждого типа со слотами методов для каждого метода, и о том факте, что для каждого интерфейса есть дополнительный список слотов методов, которые указывают на связанные реализации метода интерфейса. Но я не понимаю следующего: как CLR эффективно определяет, какой список слотов метода интерфейса выбрать из VTable типа?

Статья Drill Into. NET Framework Internals, чтобы увидеть, как CLR создает объекты среды выполнения из майского выпуска журнала MSDN Magazine за 2005 г. рассказывает о таблице сопоставления уровня процесса IVMap, проиндексированной по идентификатору интерфейса. Означает ли это, что все типы в одном процессе имеют один и тот же указатель на один и тот же IVMap?

В нем также говорится, что:

Если MyInterface1 реализуется двумя классами, в таблице IVMap будет две записи. Запись будет указывать на начало подтаблицы, встроенной в таблицу методов MyClass.

Как CLR узнает, какую запись выбрать? Выполняется ли линейный поиск для поиска записи, соответствующей текущему типу? Или бинарный поиск? Или какая-то прямая индексация и карта с возможно большим количеством пустых записей в ней?

Я также читал главу «Интерфейсы в CLR через 3-е издание C #», но в ней не говорится об этом. Поэтому ответы на этот другой вопрос не отвечают на мои вопрос.


person Daniel A.A. Pelsmaeker    schedule 21.03.2012    source источник
comment
Ссылка в вопросе больше не указывает на конкретную статью. Не могли бы вы рассказать нам, в каком номере вышла статья (месяц и год)?   -  person    schedule 14.12.2015
comment
@buffjape Это был майский выпуск журнала журнала MSDN за май 2005 года. Я обновил ссылку, чтобы вместо этого указывать на интернет-архив.   -  person Daniel A.A. Pelsmaeker    schedule 15.12.2015
comment
Пожалуйста, не назначайте большую награду за сильно устаревший ответ на копирование и вставку, который не был актуален даже на момент публикации. Это никому не помогает.   -  person Hans Passant    schedule 30.10.2016
comment
Много интересной информации можно найти в ответе на этот вопрос: softwareengineering.stackexchange.com/questions/373806/   -  person tigrou    schedule 06.10.2018


Ответы (3)


. NET Stack

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

Означает ли это, что все типы в одном процессе имеют один и тот же указатель на один и тот же IVMap?

Да, поскольку он находится на уровне домена, это означает, что все в этом AppDomain имеет одинаковую IVMap.

Как CLR узнает, какую запись выбрать? Выполняется ли линейный поиск для поиска записи, соответствующей текущему типу? Или бинарный поиск? Или какая-то прямая индексация и карта с возможно большим количеством пустых записей в ней?

Классы расположены со смещениями, поэтому у всего есть относительно заданная область, где это должно быть. Это упрощает поиск методов. Он будет искать в таблице IVMap и находить этот метод из интерфейса. Оттуда он переходит в MethodSlotTable и использует реализацию интерфейса этого класса. Карта интерфейса для класса содержит метаданные, однако реализация обрабатывается так же, как и любой другой метод.

Снова с сайта, на который вы указали:

Каждая реализация интерфейса будет иметь запись в IVMap. Если MyInterface1 реализуется двумя классами, в таблице IVMap будет две записи. Запись будет указывать на начало подтаблицы, встроенной в таблицу метода MyClass.

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


РЕДАКТИРОВАТЬ: чтобы предоставить дополнительную информацию о IVMap.

Опять же, по ссылке в OP:

Первые 4 байта первой записи InterfaceInfo указывают на TypeHandle MyInterface1 (см. Рисунок 9 и рисунок 10). Следующее СЛОВО (2 байта) занимают флаги (где 0 наследуется от родителя, а 1 реализуется в текущем классе). СЛОВО сразу после флагов - это начальный слот, который используется загрузчиком классов для размещения подтаблицы реализации интерфейса.

Итак, у нас есть таблица, где число - это смещение байтов. Это всего лишь одна запись в IVMap:

+----------------------------------+
| 0 - InterfaceInfo                |
+----------------------------------+
| 4 - Parent                       |
+----------------------------------+
| 5 - Current Class                |
+----------------------------------+
| 6 - Start Slot (2 Bytes)         |
+----------------------------------+

Предположим, что в этом домене приложений есть 100 интерфейсных записей, и нам нужно найти реализацию для каждой из них. Мы просто сравниваем 5-й байт, чтобы увидеть, соответствует ли он нашему текущему классу, и если это так, мы переходим к коду в 6-м байте. Поскольку каждая запись имеет длину 8 байт, нам нужно будет сделать что-то вроде этого: (Psuedocode)

findclass :
   if (!position == class) 
      findclass adjust offset by 8 and try again

Хотя это все еще линейный поиск, на самом деле он не займет так много времени, если размер повторяемых данных невелик. Надеюсь, это поможет.


РЕДАКТИРОВАТЬ2:

Поэтому, посмотрев на диаграмму и задавшись вопросом, почему в IVMap нет слота 1 для класса на диаграмме, я перечитал раздел и обнаружил следующее:

IVMap создается на основе информации карты интерфейса, встроенной в таблицу методов. Карта интерфейса создается на основе метаданных класса в процессе компоновки MethodTable. После завершения загрузки типов в диспетчере методов используется только IVMap.

Таким образом, IVMap для класса загружается только с интерфейсами, которые наследует конкретный класс. Похоже, он копирует из IVMap домена, но сохраняет только те интерфейсы, на которые указывают. Это поднимает другой вопрос, как? Скорее всего, это эквивалент того, как C ++ делает vtables, где каждая запись имеет смещение, а карта интерфейса предоставляет список смещений для включения в IVMap.

Если мы посмотрим на IVMap, это может быть для всего этого домена:

+-------------------------+
| Slot 1 - YourInterface  |
+-------------------------+
| Slot 2 - MyInterface    |
+-------------------------+
| Slot 3 - MyInterface2   |
+-------------------------+
| Slot 4 - YourInterface2 |
+-------------------------+

Предположим, что в этом домене есть только 4 реализации карты интерфейса. У каждого слота будет смещение (аналогично записи IVMap, которую я опубликовал ранее), и IVMap для этого класса будет использовать эти смещения для доступа к записи в IVMap.

Предположим, что каждый слот составляет 8 байтов, причем слот 1 начинается с 0, поэтому, если бы мы хотели получить слот 2 и 3, мы бы сделали что-то вроде этого:

mov ecx,edi
mov eax, dword ptr [ecx]
mov eax, dword ptr [ecx+08h] ; slot 2
; do stuff with slot 2
mov eax, dword ptr [ecx+10h] ; slot 3
; do stuff with slot 3

Извините, пожалуйста, мой x86, так как я не очень знаком с ним, но я попытался скопировать то, что у них есть в статье, на которую была ссылка.

person Jetti    schedule 21.03.2012
comment
Я не следую , выложенному со смещениями, поэтому у всего есть относительно заданная область, где это должно быть. Но да, я могу представить, что CLR выполняет линейный поиск по IVMap, как указано в моем сообщении. Но я хочу знать, как это работает на практике. Конечно, для чего-то вроде вызовов методов интерфейса, которые могут происходить так часто, выполнение швов линейного поиска ужасно наивно. - person Daniel A.A. Pelsmaeker; 21.03.2012
comment
@Virtlink Если вы посмотрите на изображение выше, вы заметите, что это число, основанное на куче для таблицы методов. Таким образом, GCInfo имеет значение -12, базовый размер экземпляра равен 4 и т. Д. Используя эти установленные стандартизированные смещения, вы могли бы найти поля, просто выполнив что-то вроде dword ptr [eax+0Ch] - person Jetti; 21.03.2012
comment
Ладно, да, вы правы. Но я нигде в своем вопросе не упоминал слово fields :). Я хочу знать, как выглядит IVMap и как среда CLR использует его для получения из известного интерфейса + индекса слота метода и ссылки объекта на подтаблицу в VTable типа объекта, содержащую слоты методов интерфейса. - person Daniel A.A. Pelsmaeker; 22.03.2012
comment
@Virtlink, пожалуйста, посмотрите мою редакцию. Надеюсь, я прояснил ситуацию. Если нет, дайте мне знать. - person Jetti; 22.03.2012
comment
Пример x86 под в этом заголовке не показывает ничего, что напоминает линейный поиск. У вас есть источники? - person Daniel A.A. Pelsmaeker; 22.03.2012
comment
@Virtlink Пример x86 не показывает поиск класса, он просто показывает, как выполнить реализацию интерфейса определенных классов, а это не то, что вы просили. Я не на 100% уверен, как это можно будет найти, держу пари, что кто-нибудь из MS сможет дать 100% конкретику, но не знаю, сделает ли это. Я просто указываю на то, что линейный поиск не так уж плох, как кажется в теории, исходя из макета IVMap. - person Jetti; 22.03.2012
comment
@Virtlink спасибо, я снова отредактировал. Может быть, придет больше ясности - person Jetti; 22.03.2012

Этой статье более 10 лет, и с тех пор многое изменилось.

IVMaps теперь заменены Virtual Stub Dispatch .

Диспетчеризация виртуальных заглушек (VSD) - это метод использования заглушек для вызовов виртуальных методов вместо традиционной таблицы виртуальных методов. В прошлом диспетчеризация интерфейсов требовала, чтобы интерфейсы имели уникальные идентификаторы процесса и чтобы каждый загруженный интерфейс был добавлен в карту виртуальной таблицы глобального интерфейса.

Прочтите эту статью, в ней есть более подробные сведения, которые вам когда-либо понадобятся. Он взят из Книги времени выполнения, документация, изначально написанная Разработчики CLR для разработчиков CLR, но теперь он опубликован для всех. Это в основном описывает внутренности среды выполнения.

Мне нет смысла дублировать статью здесь, но я просто изложу основные моменты и то, что они подразумевают:

  • Когда JIT видит вызов элемента интерфейса, он компилирует его в заглушку поиска. Этот фрагмент кода вызывает универсальный преобразователь.
  • Универсальный преобразователь - это функция, которая определяет, какой метод вызывать. Это наиболее общий и, следовательно, самый медленный способ вызвать такой метод. При первом вызове из заглушки поиска она исправляет эту заглушку (переписывает ее код во время выполнения) в заглушку отправки. Он также создает заглушку разрешения для дальнейшего использования. На этом этапе заглушка поиска исчезает.
  • Заглушка диспетчеризации - это самый быстрый способ вызвать член интерфейса, но есть одна загвоздка: он оптимистично считает вызов мономорфным, что означает, что он оптимизирован для случая, когда вызов интерфейса всегда разрешается к одному и тому же конкретному типу. Он сравнивает таблицу методов (то есть конкретный тип) объекта с ранее просмотренной таблицей (которая жестко запрограммирована в заглушке) и вызывает кэшированный метод (адрес которого также жестко задан), если сравнение успешно. В случае сбоя он возвращается к заглушке разрешения.
  • Заглушка resolve обрабатывает полиморфные вызовы (общий случай). Он использует кеш, чтобы найти, какой метод вызвать. Если метода нет в кэше, он вызывает универсальный преобразователь (который также записывает в этот кеш).

И вот важное соображение, прямо из статьи:

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

Среда выполнения очень оптимистично в отношении мономорфных сайтов вызова, что имеет большой смысл в реальном коде, и будет стараться избегать разрешить заглушки, насколько это возможно.

person Lucas Trzesniewski    schedule 30.10.2016

Из первой статьи, на которую вы указали:

Если MyInterface1 реализуется двумя классами, в таблице IVMap будет две записи. Запись будет указывать на начало вложенной таблицы, встроенной в таблицу метода MyClass, как показано на рисунке 9.

и

ClassLoader просматривает метаданные текущего класса, родительского класса и интерфейсов и создает таблицу методов. В процессе компоновки он заменяет любые переопределенные виртуальные методы, заменяет любые скрытые методы родительского класса, создает новые слоты и при необходимости дублирует слоты. Дублирование слотов необходимо для создания иллюзии, что у каждого интерфейса есть своя мини-таблица. Однако дублированные слоты указывают на одну и ту же физическую реализацию.

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

Хотя может быть и совершенно неправильным.

person Chris Shain    schedule 21.03.2012
comment
Если вы прочитали мой пост полностью, вы бы увидели, что я уже цитировал ту же цитату, что и вы, и что я знаю, как в VTable есть подраздел для каждого интерфейса с указателями на реализации методов. У меня конкретный вопрос: как среда CLR знает, какой подраздел использовать. - person Daniel A.A. Pelsmaeker; 21.03.2012