Этот тест показывает, что вызов виртуального метода непосредственно по ссылке на объект быстрее, чем вызов его на ссылку на интерфейс, этот объект реализует.
Другими словами:
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();
}
Исходя из мира С++, я ожидал, что оба эти вызова будут реализованы идентично (как простой поиск виртуальной таблицы) и будут иметь одинаковую производительность. Как С# реализует виртуальные вызовы и что это за "лишняя" работа, которая, по-видимому, выполняется при вызове через интерфейс?
--- EDIT ---
ОК, ответы/комментарии, которые я получил до сих пор, подразумевают, что существует двойной разворот указателя для виртуального вызова через интерфейс по сравнению с одним разыменованием для виртуального вызова через объект.
Так может понравиться кто-нибудь объяснить , почему это необходимо? Какова структура виртуальной таблицы в С#? Является ли он "плоским" (как это типично для С++) или нет? Каковы были компромиссы в дизайне, которые были сделаны на языке С#, что привело к этому? Я не говорю, что это "плохой" дизайн, мне просто интересно, почему это было необходимо.
В двух словах я хотел бы понять, что мой инструмент делает под капотом, поэтому я могу использовать его более эффективно. И я был бы признателен, если бы я больше не получал ответы типа "вы не должны знать этого" или "использовать другой язык".
--- EDIT 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
}
}
--- EDIT 3 ---
Если кому-то интересно, вот как мой Visual С++ 2010 выдает экземпляр класса, который наследует другие классы:
код:
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;
}
};
Debugger:
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-битной сборке).
...
C c;
std::cout << static_cast<IA*>(&c) << std::endl;
std::cout << static_cast<IB*>(&c) << std::endl;
.. печатает...
0027F778
0027F77C
... указывая, что указатели на разные интерфейсы внутри одного и того же объекта фактически указывают на разные части этого объекта (т.е. содержат разные физические адреса).