Подтвердить что ты не робот

Call and Callvirt

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

4b9b3361

Ответ 1

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

Ответ 2

Когда среда выполнения выполняет инструкцию 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 является закрытым классом и не может иметь подклассы.

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

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

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

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

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

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

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

РЕДАКТИРОВАТЬ 2: Там больше, чем кажется. Например, следующий код генерирует команду call:

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

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

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

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

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

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

Ответ 3

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

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

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

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

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();
    }
}

ПРИМЕЧАНИЕ Для этого не нужно закрывать класс.

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

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

Ответ 4

В соответствии с MSDN:

Call:

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

CallVirt:

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

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

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

CallVirt: variable → экземпляр объекта → объект объекта объекта → метод

Ответ 5

Одна вещь, возможно, стоит добавить к предыдущим ответам, кажется, есть только одно лицо к тому, как "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, <pObj>
    call        (direct-ptr-to) <TestExt.FExt>

    pObj.FInst(); // IL:callvirt[instance]
    mov         rax, <pObj>
    cmp         byte ptr [rax],0
    mov         rcx, <pObj>
    call        (direct-ptr-to) <Test.FInst>

    pObj.FVirt(); // IL:callvirt[virtual]
    mov         rax, <pObj>
    mov         rax, qword ptr [rax]  
    mov         rax, qword ptr [rax + NNN]  
    mov         rcx, <pObj>
    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, а затем к фактическому слоту)

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

Ответ 6

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

Ссылка:

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

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