С#: Почему вызов метода реализованного интерфейса быстрее для переменной класса, чем для переменной интерфейса?

Я нашел это странное поведение в .NET и даже после того, как снова просмотрел CLR через С#, я все еще запутался. Предположим, что у нас есть интерфейс с одним методом и классом, который его реализует:

interface IFoo
{
    void Do();
}

class TheFoo : IFoo
{
    public void Do()
    {
        //do nothing
    }
}

Затем мы хотим просто создать экземпляр этого класса и вызвать этот метод Do() много раз двумя способами: используя конкретную переменную класса и используя переменную интерфейса:

TheFoo foo1 = new TheFoo();

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (long i = 0; i < 1000000000; i++)
    foo1.Do();
stopwatch.Stop();
Console.Out.WriteLine("Elapsed time: " + stopwatch.ElapsedMilliseconds);

IFoo foo2 = foo1;

stopwatch = new Stopwatch();
stopwatch.Start();
for (long i = 0; i < 1000000000; i++)
    foo2.Do();
stopwatch.Stop();
Console.Out.WriteLine("Elapsed time: " + stopwatch.ElapsedMilliseconds);

Удивительно (по крайней мере для меня) прошедшие времена примерно на 10% различны:

Elapsed time: 6005
Elapsed time: 6667

Разница не так уж и много, поэтому я бы не стал об этом беспокоиться в большинстве случаев. Однако я просто не могу понять, почему это происходит даже после просмотра кода IL, поэтому я был бы признателен, если бы кто-то указал мне на что-то очевидное, что мне не хватает.

Ответы

Ответ 1

Вы должны посмотреть машинный код, чтобы узнать, что происходит. Когда вы это сделаете, вы увидите, что оптимизатор дрожания полностью удалил вызов foo1.Do(). Небольшие методы, подобные этому, устанавливаются оптимизатором. Поскольку тело метода не содержит кода, машинный код вообще не создается. Он не может сделать такую ​​же оптимизацию на интерфейсе, он недостаточно умен, чтобы перепроектировать, что указатель метода интерфейса фактически указывает на пустой метод.

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

ПРИМЕЧАНИЕ: просмотр машинного кода в сборке релизов требует изменения опции. По умолчанию оптимизатор отключается при отладке кода даже в сборке релизов. Инструменты + Опции, Отладка, Общие, отключить "Подавлять оптимизацию JIT при загрузке модуля".

Ответ 2

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

Итак, когда CLR сталкивается с вызовом интерфейса, он видит в интерфейсных сопоставлениях охватывающего типа и проверяет, какой конкретный метод он должен вызывать. Это на самом деле ниже IL.

UPD: IMO - это не разница между call и callvirt.

Что должно делать CLR, когда встречается callvirt в классе? Получите тип вызываемого абонента, посмотрите его таблицу виртуальных методов, найдите там вызываемый метод и назовите его.

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

UPD2. Теперь я уверен, что это так. См. этот для фактической реализации.