Производительность" прямого " виртуального вызова по сравнению с вызовом интерфейса в C#



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



другими словами:



interface IFoo {
void Bar();
}

class Foo : IFoo {
public virtual void Bar() {}
}

void Benchmark() {
Foo f = new Foo();
IFoo f2 = f;
f.Bar(); // This is faster.
f2.Bar();
}


исходя из мира C++, я ожидал бы, что оба этих вызова будут реализованы одинаково (как простой поиск виртуальной таблицы) и имеют одинаковую производительность. Как C# реализует виртуальные вызовы и что это такое " дополнительно" работа, которая, по-видимому, выполняется при вызове через интерфейс?



--- редактировать ---



хорошо, ответы / комментарии, которые я получил до сих пор, подразумевают, что существует разыменование двойного указателя для виртуального вызова через интерфейс по сравнению с одним разыменованием для виртуального вызова через объект.



так может кто-нибудь объяснить почему это обязательно? Какова структура виртуальной таблицы в C#? Это "плоский" (как это типично для C++) или нет? Какие были компромиссы дизайна, которые были сделаны в дизайне языка C#, которые приводят к этому? Я не говорю, что это "плохой" дизайн, мне просто любопытно, почему это было необходимо.



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



--- редактировать 2 - - -



просто, чтобы было ясно, что мы не имея дело с некоторым компилятором JIT-оптимизации здесь, который удаляет динамическую отправку: я изменил тест, упомянутый в исходном вопросе, чтобы создать экземпляр одного класса или другого случайным образом во время выполнения. Поскольку создание экземпляра происходит после компиляции и после загрузки сборки / JITing, нет никакого способа избежать динамической отправки в обоих случаях:



interface IFoo {
void Bar();
}

class Foo : IFoo {
public virtual void Bar() {
}
}

class Foo2 : Foo {
public override void Bar() {
}
}

class Program {

static Foo GetFoo() {
if ((new Random()).Next(2) % 2 == 0)
return new Foo();
return new Foo2();
}

static void Main(string[] args) {

var f = GetFoo();
IFoo f2 = f;

Console.WriteLine(f.GetType());

// JIT warm-up
f.Bar();
f2.Bar();

int N = 10000000;
Stopwatch sw = new Stopwatch();

sw.Start();
for (int i = 0; i < N; i++) {
f.Bar();
}
sw.Stop();
Console.WriteLine("Direct call: {0:F2}", sw.Elapsed.TotalMilliseconds);

sw.Reset();
sw.Start();
for (int i = 0; i < N; i++) {
f2.Bar();
}
sw.Stop();
Console.WriteLine("Through interface: {0:F2}", sw.Elapsed.TotalMilliseconds);

// Results:
// Direct call: 24.19
// Through interface: 40.18

}

}


--- редактировать 3 - - -



если кто-то заинтересован, вот как мой Visual C++ 2010 выкладывает экземпляр a класс, который многократно наследует другие классы:



код:



class IA {
public:
virtual void a() = 0;
};

class IB {
public:
virtual void b() = 0;
};

class C : public IA, public IB {
public:
virtual void a() override {
std::cout << "a" << std::endl;
}
virtual void b() override {
std::cout << "b" << std::endl;
}
};


отладчик:



c   {...}   C
IA {...} IA
__vfptr 0x00157754 const C::`vftable'{for `IA'} *
[0] 0x00151163 C::a(void) *
IB {...} IB
__vfptr 0x00157748 const C::`vftable'{for `IB'} *
[0] 0x0015121c C::b(void) *


несколько указателей виртуальной таблицы хорошо видны, и sizeof(C) == 8 (в 32-битной сборке).



The...



C c;
std::cout << static_cast<IA*>(&c) << std::endl;
std::cout << static_cast<IB*>(&c) << std::endl;


..печать...



0027F778
0027F77C


...указывая, что указатели на разные интерфейсы внутри одного и того же объекта фактически указывают на разные части этого объекта (т. е. они содержат разные физические адреса).

658   5  

5 ответов:

Я думаю, что статья в http://msdn.microsoft.com/en-us/magazine/cc163791.aspx ответит на ваши вопросы. В частности, смотрите раздел интерфейс Vtable Map и интерфейс Map, и следующий раздел о виртуальной диспетчеризации.

вероятно, JIT-компилятор может разобраться и оптимизировать код для вашего простого случая. Но не в общем случае.

IFoo f2 = GetAFoo();

и GetAFoo определяется как возврат IFoo, затем JIT-компилятор не сможет оптимизировать вызов.

вот как выглядит dis-сборка (Ганс прав):

            f.Bar(); // This is faster.
00000062  mov         rax,qword ptr [rsp+20h] 
00000067  mov         rax,qword ptr [rax] 
0000006a  mov         rcx,qword ptr [rsp+20h] 
0000006f  call        qword ptr [rax+60h] 
            f2.Bar();
00000072  mov         r11,7FF000400A0h 
0000007c  mov         qword ptr [rsp+38h],r11 
00000081  mov         rax,qword ptr [rsp+28h] 
00000086  cmp         byte ptr [rax],0 
00000089  mov         rcx,qword ptr [rsp+28h] 
0000008e  mov         r11,qword ptr [rsp+38h] 
00000093  mov         rax,qword ptr [rsp+38h] 
00000098  call        qword ptr [rax] 

Я попробовал ваш тест и на моей машине, в определенном контексте, результат на самом деле наоборот.

Я запускаю Windows 7 x64, и я создал проект консольного приложения Visual Studio 2010, в который я скопировал ваш код. Если скомпилировать проект в отладки режим и с целью платформы как x86 вывод будет следующий:

прямой вызов: 48.38
Через интерфейсы: 42.43

на самом деле каждый раз при запуске приложения он будет предоставлять немного разные результаты, но вызовы интерфейса всегда будут быстрее. Я предполагаю, что поскольку приложение скомпилировано как x86, оно будет запускаться ОС через WOW.

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

релиз режим и x86 цель
Прямой вызов: 23.02
Через интерфейс: 32.73

Debug режим и x64 цель
Прямой вызов: 49.49
Через интерфейс: 56.97

релиз режим и x64 цель
Прямой вызов: 19.60
Через интерфейс: 26.45

все вышеперечисленные тесты были выполнены с .Net 4.0 в качестве целевой платформы для компилятора. При переключении на 3.5 и повторении вышеуказанных тестов, вызовы через интерфейс всегда был длиннее, чем прямые вызовы.

Итак, приведенные выше тесты довольно усложняют ситуацию, поскольку кажется, что поведение, которое вы заметили, не всегда происходит.

В конце концов, с риском расстроить вас, я хотел бы добавить несколько мыслей. Многие люди добавили комментарии, что различия в производительности довольно малы и в реальном мире программирования вы не должны заботиться о них, и я согласен с этой точкой зрения. Есть две основные причины оно.

первым и наиболее рекламируемым является то, что .Net был построен на более высоком уровне, чтобы разработчики могли сосредоточиться на более высоких уровнях приложений. Вызов базы данных или внешней службы в тысячи, а иногда и в миллионы раз медленнее, чем вызов виртуального метода. Имея хорошую архитектуру высокого уровня и фокусируясь на большой производительности потребителей всегда принесет лучшие результаты в современных приложениях, а не избежать двойной указатель разыменовывает.

второй и более неясный заключается в том, что команда .Net, построив фреймворк на более высоком уровне, фактически представила ряд уровней абстракции, которые компилятор just in time сможет использовать для оптимизации на разных платформах. Чем больше доступа они дадут нижним слоям, тем больше разработчики смогут оптимизировать для конкретной платформы, но тем меньше компилятор времени выполнения сможет сделать для других. То есть по крайней мере, теория, и именно поэтому все не так хорошо документировано, как в C++ в отношении этого конкретного вопроса.

Я думаю, что чистый случай виртуальной функции может использовать простую таблицу виртуальных функций, как и любой производный класс Foo реализация Bar просто изменил бы указатель виртуальной функции на Bar.

С другой стороны, вызов функции интерфейса IFoo:Bar не мог выполнить поиск в чем-то вроде IFooвиртуальная таблица функций, потому что каждая реализация IFoo не нужно обязательно реализовывать другие функции или интерфейсы, которые Foo делает. Так позиция входа в таблицу виртуальных функций для Bar С другой class Fubar: IFoo не должно совпадать с позицией входа в виртуальную таблицу функций Bar in class Foo:IFoo.

общее правило: занятия проходят быстро. Интерфейсы работают медленно.

Это одна из причин рекомендации "построения иерархий классов и использование интерфейсов внутри иерархии поведения".

для виртуальных методов, разница может быть небольшой (например, 10%). Но для невиртуальных методов и полей разница огромна. Рассмотрим эту программу.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace InterfaceFieldConsoleApplication
{
    class Program
    {
        public abstract class A
        {
            public int Counter;
        }

        public interface IA
        {
            int Counter { get; set; }
        }

        public class B : A, IA
        {
            public new int Counter { get { return base.Counter; } set { base.Counter = value; } }
        }

        static void Main(string[] args)
        {
            var b = new B();
            A a = b;
            IA ia = b;
            const long LoopCount = (int) (100*10e6);
            var stopWatch = new Stopwatch();
            stopWatch.Start();
            for (int i = 0; i < LoopCount; i++)
                a.Counter = i;
            stopWatch.Stop();
            Console.WriteLine("a.Counter: {0}", stopWatch.ElapsedMilliseconds);
            stopWatch.Reset();
            stopWatch.Start();
            for (int i = 0; i < LoopCount; i++)
                ia.Counter = i;
            stopWatch.Stop();
            Console.WriteLine("ia.Counter: {0}", stopWatch.ElapsedMilliseconds);
            Console.ReadKey();
        }
    }
}

выход:

a.Counter: 1560
ia.Counter: 4587

Comments

    Ничего не найдено.