C #: Почему вызов реализованного метода интерфейса быстрее для переменной класса, чем для переменной интерфейса? - PullRequest
15 голосов
/ 18 июня 2011

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

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, поэтому я был бы признателен, если бы кто-то указал мне на что-то очевидное, что я скучаю.

Ответы [ 2 ]

18 голосов
/ 18 июня 2011

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

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

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

1 голос
/ 18 июня 2011

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

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...