Call and Callvirt

В чем разница между инструкциями CIL «Call» и «Callvirt»?

    call предназначен для вызова не виртуальных, статических или суперclassных методов, т. е. цель вызова не подлежит переопределению. callvirt предназначен для вызова виртуальных методов (так что если this подclass, который переопределяет метод, вместо него вызывается версия подclassа).

    Когда исполняемая команда выполняет команду call она делает вызов для точной части кода (метода). Там нет вопроса о том, где он существует. После того, как IL был JITted, полученный машинный код на сайте вызова является безусловной командой jmp .

    Напротив, команда callvirt используется для вызова виртуальных методов полиморфным способом. Точное местоположение кода метода должно определяться во время выполнения для каждого вызова. Полученный JIT-код включает некоторую косвенность через vtable-структуры. Следовательно, вызов выполняется медленнее, но он более гибкий, поскольку он допускает полиморфные вызовы.

    Обратите внимание, что компилятор может генерировать инструкции call для виртуальных методов. Например:

     sealed class SealedObject : object { public override bool Equals(object o) { // ... } } 

    Рассмотрим код:

     SealedObject a = // ... object b = // ... bool equal = a.Equals(b); 

    Хотя System.Object.Equals(object) является виртуальным методом, в этом использовании нет способа перегрузки метода Equals . SealedObject является закрытым classом и не может иметь подclassы.

    По этой причине sealed classы .NET могут иметь лучшую скорость отправки сообщений, чем их незапечатанные копии.

    EDIT: Оказывается, я был неправ. Компилятор C # не может выполнить безусловный переход к местоположению метода, потому что ссылка объекта (значение this в методе) может быть нулевой. Вместо этого он выдает callvirt который выполняет нулевую проверку и бросает, если требуется.

    На самом деле это объясняет какой-то причудливый код, который я нашел в платформе .NET, используя Reflector:

     if (this==null) // ... 

    Компилятор может испускать проверяемый код, который имеет нулевое значение для this указателя (local0), только csc не делает этого.

    Поэтому я предполагаю, что call используется только для статических методов и структур classа.

    Учитывая эту информацию, мне кажется, что sealed полезен только для безопасности API. Я нашел еще один вопрос, который, по-видимому, свидетельствует о том, что нет никаких преимуществ в производительности для уплотнения ваших classов.

    EDIT 2: Это больше, чем кажется. Например, следующий код выдает команду call :

     new SealedObject().Equals("Rubber ducky"); 

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

    Интересно, что в сборке DEBUG следующий код испускает callvirt :

     var o = new SealedObject(); o.Equals("Rubber ducky"); 

    Это связано с тем, что вы можете установить точку останова на второй строке и изменить значение o . В релизах я предполагаю, что вызов будет скорее call , чем callvirt .

    К сожалению, мой компьютер в настоящее время не работает, но я буду экспериментировать с ним, как только он снова появится.

    По этой причине запечатанные classы .NET могут иметь лучшую скорость отправки сообщений, чем их незапечатанные копии.

    К сожалению, это не случай. Callvirt делает еще одну вещь, которая делает ее полезной. Когда у объекта есть метод, вызываемый на нем, callvirt проверяет, существует ли объект, и если не выбрасывает исключение NullReferenceException. Вызов просто переместится в ячейку памяти, даже если ссылка на объект отсутствует, и попытайтесь выполнить байты в этом месте.

    Это означает, что callvirt всегда используется компилятором C # (не уверен в VB) для classов, и вызов всегда используется для structs (потому что они никогда не могут быть нулевыми или подclassами).

    Edit В ответ на комментарий Drew Noakes: Да, похоже, вы можете заставить компилятор выпустить вызов для любого classа, но только в следующем очень конкретном случае:

     public class SampleClass { public override bool Equals(object obj) { if (obj.ToString().Equals("Rubber Ducky", StringComparison.InvariantCultureIgnoreCase)) return true; return base.Equals(obj); } public void SomeOtherMethod() { } static void Main(string[] args) { // This will emit a callvirt to System.Object.Equals bool test1 = new SampleClass().Equals("Rubber Ducky"); // This will emit a call to SampleClass.SomeOtherMethod new SampleClass().SomeOtherMethod(); // This will emit a callvirt to System.Object.Equals SampleClass temp = new SampleClass(); bool test2 = temp.Equals("Rubber Ducky"); // This will emit a callvirt to SampleClass.SomeOtherMethod temp.SomeOtherMethod(); } } 

    ПРИМЕЧАНИЕ Для этого class не должен быть запечатан.

    Таким образом, похоже, что компилятор выдает вызов, если все это верно:

    • Вызов метода сразу после создания объекта
    • Метод не реализован в базовом classе

    Согласно MSDN:

    Звоните :

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

    CallVirt :

    Команда callvirt вызывает метод поздней привязки для объекта. То есть, метод выбирается на основе типа времени выполнения obj, а не classа времени компиляции, видимого в указателе метода . Callvirt может использоваться для вызова как виртуальных, так и методов экземпляра.

    Таким образом, в основном используются разные маршруты для вызова метода экземпляра объекта, переопределения или нет:

    Вызов: переменная -> объект типа переменной -> метод

    CallVirt: variable -> object instance -> объект типа объекта -> метод

    Одна вещь, возможно, стоит добавить к предыдущим ответам, есть, по-видимому, только одно лицо, как «IL call» на самом деле выполняется, а два лица – как «IL callvirt».

    Возьмите эту настройку.

      public class Test { public int Val; public Test(int val) { Val = val; } public string FInst () // note: this==null throws before this point { return this == null ? "NO VALUE" : "ACTUAL VALUE " + Val; } public virtual string FVirt () { return "ALWAYS AN ACTUAL VALUE " + Val; } } public static class TestExt { public static string FExt (this Test pObj) // note: pObj==null passes { return pObj == null ? "NO VALUE" : "VALUE " + pObj.Val; } } 

    Во-первых, тело CIL FInst () и FExt () на 100% идентично, opcode-to-opcode (за исключением того, что один объявлен «экземпляр», а другой «статический»), однако FInst () будет вызван с «callvirt» и FExt () с «вызовом».

    Во-вторых, FInst () и FVirt () будут вызываться с помощью «callvirt» – хотя один из них виртуальный, а другой – нет, но это не тот же самый callvirt, который действительно сможет выполнить.

    Вот что примерно происходит после JITting:

      pObj.FExt(); // IL:call mov rcx,  call (direct-ptr-to)  pObj.FInst(); // IL:callvirt[instance] mov rax,  cmp byte ptr [rax],0 mov rcx,  call (direct-ptr-to)  pObj.FVirt(); // IL:callvirt[virtual] mov rax,  mov rax, qword ptr [rax] mov rax, qword ptr [rax + NNN] mov rcx,  call qword ptr [rax + MMM] 

    Единственная разница между «вызовом» и «callvirt [instance]» заключается в том, что «callvirt [instance]» намеренно пытается получить доступ к одному байту с * pObj, прежде чем он вызовет прямой указатель функции экземпляра (чтобы, возможно, выбросить исключение) прямо тут же »).

    Таким образом, если вас раздражает количество раз, когда вы должны написать «контрольную часть»

     var d = GetDForABC (a, b, c); var e = d != null ? d.GetE() : ClassD.SOME_DEFAULT_E; 

    Вы не можете нажать «if (this == null) return SOME_DEFAULT_E;» вниз в ClassD.GetE () (поскольку семантика IL callvirt [instance] “запрещает вам это делать), но вы можете вставлять ее в .GetE (), если вы перемещаете .GetE () в функцию расширения где-то (поскольку семантика «IL call» позволяет это, но, увы, потерять доступ к частным членам и т. д.),

    Тем не менее, выполнение «callvirt [instance]» имеет больше общего с «вызовом», чем с «callvirt [virtual]», поскольку последнему может потребоваться тройная косвенность, чтобы найти адрес вашей функции. (косвенность к базе typedef, затем к base-vtab-or-some-interface, затем к фактическому слоту)

    Надеюсь, это поможет, Борис

    Просто добавив к вышеуказанным ответам, я думаю, что изменение было сделано давно, так что команда Callvirt IL будет генерироваться для всех методов экземпляра, и команда Call IL будет генерироваться для статических методов.

    Справка :

    Курс Pluralsight «Внутренние языки C # – часть 1 Барта Де Смета (видео – инструкции вызова и стеки вызовов в CLR IL в двух словах)

    а также https://blogs.msdn.microsoft.com/ericgu/2008/07/02/why-does-c-always-use-callvirt/

    Давайте будем гением компьютера.