Этот тест , кажется, показывает, что вызов виртуального метода непосредственно по ссылке на объект происходит быстрее, чем вызов его по ссылке на интерфейс, который реализует этот объект.
Другими словами:
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 выкладывает экземпляр класса, который наследует несколько классов:
Код:
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-разрядной сборке).
The ...
C c;
std::cout << static_cast<IA*>(&c) << std::endl;
std::cout << static_cast<IB*>(&c) << std::endl;
.. печать ...
0027F778
0027F77C
... указывает, что указатели на разные интерфейсы внутри одного и того же объекта фактически указывают на разные части этого объекта (т. Е. Они содержат разные физические адреса).