Почему компилятор C # испускает инструкцию callvirt для вызова метода GetType ()? - PullRequest
28 голосов
/ 10 мая 2009

Мне любопытно узнать, почему это происходит. Пожалуйста, ознакомьтесь с примером кода ниже и соответствующим IL, который был создан в комментариях под каждым разделом:

using System;

class Program
{
    static void Main()
    {
        Object o = new Object();
        o.GetType();

        // L_0001: newobj instance void [mscorlib]System.Object::.ctor()
        // L_0006: stloc.0 
        // L_0007: ldloc.0 
        // L_0008: callvirt instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()

        new Object().GetType();

        // L_000e: newobj instance void [mscorlib]System.Object::.ctor()
        // L_0013: call instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
    }
}

Почему компилятор выдал callvirt для первого раздела, а call для второго раздела? Есть ли какая-то причина, по которой компилятор будет выдавать инструкцию callvirt для не виртуального метода? И если есть случаи, когда компилятор выдаст callvirt для не виртуального метода, это создает проблемы для безопасности типов?

Ответы [ 5 ]

28 голосов
/ 10 мая 2009

См. это старое сообщение в блоге Эрика Ганнерсона.

Вот текст поста:

Почему C # всегда использует callvirt?

Этот вопрос возник из-за внутреннего псевдонима C #, и я подумал, что ответ будет представлять общий интерес. Это при условии, что ответ правильный - прошло довольно много времени.

Язык .NET IL предоставляет инструкции call и callvirt, причем callvirt используется для вызова виртуальных функций. Но если вы посмотрите на код, который генерирует C #, вы увидите, что он генерирует «callvirt» даже в тех случаях, когда виртуальная функция не задействована. Почему он это делает?

Я вернулся к заметкам о дизайне языка, которые у меня есть, и они совершенно ясно заявляют, что мы решили использовать callvirt 13.12.1999. К сожалению, они не отражают наше обоснование для этого, поэтому мне придется уйти из моей памяти.

Мы получили отчет от кого-то (вероятно, одной из групп .NET, использующих C # (хотя в то время он еще не назывался C #)), который написал код, который вызывал метод с нулевым указателем, но они не сделали этого. не получите исключения, потому что метод не имеет доступа ни к каким полям (т. е. «this» было нулевым, но ничто в методе не использовало его). Затем этот метод вызвал другой метод, который использовал эту точку и выдал исключение, после чего возникло небольшое головокружение. После того, как они поняли это, они прислали нам записку об этом.

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

22 голосов
/ 10 мая 2009

Просто играю безопасно.

Технически компилятор C # не всегда использует callvirt

Для статических методов и методов, определенных для типов значений, используется call. Большинство предоставляется с помощью инструкции callvirt IL.

Разница, которая повлияла на голосование между ними, заключается в том, что call предполагает, что "объект, используемый для вызова", не равен нулю. callvirt с другой стороны, проверяет наличие ненулевого значения и выдает исключение NullReferenceException, если это необходимо.

  • Для статических методов объект является типом объекта и не может быть нулевым. То же самое для типов значений. Следовательно, для них используется call - лучшая производительность.
  • Для остальных разработчики языка решили использовать callvirt, поэтому JIT-компилятор проверяет, что объект, используемый для выполнения вызова, не является нулевым. Даже для не виртуальных методов экземпляров ... они ценят безопасность за производительность.

См. Также: Джефф Рихтер справляется с этой задачей лучше - в своей главе «Проектирование типов» в CLR через C # 2nd Ed

3 голосов
/ 11 мая 2009

Как (возможно-) интересное ... GetType() необычно тем, что не virtual - это приводит к некоторым очень, очень странным вещам .

(помечено как wiki, поскольку оно не соответствует теме)

1 голос
/ 10 мая 2009

Компилятор не знает реальный тип o в первом выражении, но он знает реальный тип во втором выражении. Похоже, что он смотрит только на одно утверждение за раз.

Это нормально, потому что C # сильно зависит от JIT для оптимизации. Весьма вероятно, что в таком простом случае оба вызова станут вызовами экземпляра во время выполнения.

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

0 голосов
/ 10 мая 2009

Я бы рискнул предположить, что это потому, что первое присваивается переменной, которая потенциально может содержать пониженный экземпляр другого типа, который мог бы переопределить GetType (хотя мы видим, что это не так); второй никогда не может быть ничем иным, кроме Object.

...