Как работает инструкция callvirt .NET для интерфейсов?
Объяснить кому-то виртуальную диспетчеризацию легко: каждый объект имеет указатель на таблицу как часть своих данных. В классе N виртуальных методов. Каждый вызов определенного метода индексирует объект, когда он прибывает, и вызывает i-й метод в таблице. Каждый класс, реализующий метод X(), будет иметь код для метода X() с тем же i-м индексом.
Но тогда мы получаем интерфейсы. А интерфейсы требуют некоторого искажения, потому что два не наследующих класса, которые реализуют один и тот же интерфейс, будут иметь виртуальные функции в разных индексах таблицы.
Я искал в Интернете, и я могу найти много дискуссий о том, как можно реализовать диспетчеризацию интерфейса. Существует две широкие категории: а) своего рода хеш-таблица ищет объект для поиска нужной таблицы диспетчеризации; б) когда объект приводится к интерфейсу, создается новый указатель, который указывает на те же данные, но на разные виртуальные таблицы.
Но, несмотря на большое количество информации о том, как это может работать, я ничего не могу найти о том, как движок.NET действительно его реализует.
Кто-нибудь знает документ, который описывает фактическую арифметику указателей, которая происходит в инструкции callvirt, когда тип объекта является интерфейсом?
2 ответа
Диспетчеризация интерфейса в CLR - это чёрная магия.
Как вы правильно заметили, отправка виртуальных методов концептуально проста для объяснения. И на самом деле я делаю это в этой серии статей, где я описываю, как можно реализовать виртуальные методы в C#-подобном языке, в котором их нет:
Механизмы, которые я описываю, очень похожи на механизмы, которые фактически используются.
Распределение интерфейса описать гораздо сложнее, и то, как CLR его реализует, совсем не очевидно. Механизмы CLR для диспетчеризации интерфейса были тщательно настроены для обеспечения высокой производительности в наиболее распространенных ситуациях, и поэтому детали этих механизмов могут изменяться в зависимости от того, как команда CLR приобретает больше знаний о реальных шаблонах использования.
По сути, он работает за кулисами так, что на каждом сайте вызова, то есть в каждой точке кода, где вызывается метод интерфейса, есть небольшой кэш, который говорит: "Я думаю, что метод, связанный с этим интерфейсным слотом, есть... Вот". В подавляющем большинстве случаев этот кеш верен; Вы очень редко вызываете один и тот же метод интерфейса миллион раз с миллионами различных реализаций. Это обычно одна и та же реализация снова и снова, много раз подряд.
Если кеш оказывается пропущенным, он возвращается к хеш-таблице, которая поддерживается, чтобы сделать поиск немного медленнее.
Если это оказывается пропущенным, то метаданные объекта анализируются, чтобы определить, какой метод соответствует слоту интерфейса.
Чистый эффект состоит в том, что на данном сайте вызова, если вы всегда вызываете метод интерфейса, который сопоставляется с конкретным методом класса, это очень быстро. Если вы всегда вызываете один из нескольких методов класса для данного метода интерфейса, производительность довольно хорошая. Худшее, что нужно сделать, это никогда не вызывать один и тот же метод класса дважды с одним и тем же интерфейсным методом на одном сайте; каждый раз это самый медленный путь.
Если вы хотите узнать, как таблицы для медленного поиска хранятся в памяти, см. Ссылку в ответе Мэтью Уотсона.
Поскольку компилятор всегда должен иметь фактический объект, для которого вызывается метод (во время выполнения), он всегда знает во время выполнения конкретный тип, с которым он имеет дело.
Код, который вызывает виртуальный метод, сначала определяет тип используемого объекта. Затем он просматривает таблицу методов типа для поиска вызываемого метода. Затем код просто вызывает этот метод, передавая ссылку на объект как "this" вместе с любыми другими параметрами.
Я подозреваю, что ключевой момент, который вас интересует, заключается в том, как код ищет адрес метода в таблице методов типа.
Более подробная информация о таблице методов приведена в статье "JIT and Run" в майском выпуске журнала MSDN за 2005 год (которую на момент написания статьи можно было загрузить с этой страницы как ".chm"), но вам придется перейти в свойствах файла, чтобы разблокировать его, прежде чем он будет отображаться должным образом, из-за ограничений безопасности.)
О том, как именно выполняется поиск, все еще неточности, но он дает довольно много других деталей.