Ce point de référence semble montrer que l'appel d'une méthode virtuelle directement sur une référence d'objet est plus rapide que l'appel sur la référence à l'interface que cet objet implémente.
En d'autres termes :
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();
}
Venant du monde du C++, je me serais attendu à ce que ces deux appels soient implémentés de manière identique (comme une simple consultation de table virtuelle) et aient les mêmes performances. Comment le C# implémente-t-il les appels virtuels et quel est ce travail "supplémentaire" qui est apparemment effectué lors d'un appel via une interface ?
--- EDIT ---
OK, les réponses/commentaires que j'ai reçus jusqu'à présent impliquent qu'il y a une déréférence double-pointeur pour l'appel virtuel par l'interface contre une seule déréférence pour l'appel virtuel par l'objet.
Quelqu'un pourrait-il m'expliquer pourquoi est-ce nécessaire ? Quelle est la structure de la table virtuelle en C# ? Est-elle "plate" (comme c'est le cas en C++) ou non ? Quels sont les compromis de conception qui ont été faits dans la conception du langage C# et qui ont conduit à cette situation ? Je ne dis pas que c'est une "mauvaise" conception, je suis simplement curieux de savoir pourquoi elle était nécessaire.
En bref, j'aimerais comprendre ce que fait mon outil sous le capot pour que je puisse l'utiliser plus efficacement. Et j'apprécierais de ne plus recevoir de réponses du type "vous ne devriez pas savoir ça" ou "utilisez une autre langue".
--- EDIT 2 ---
Pour que les choses soient claires, nous n'avons pas affaire ici à un compilateur d'optimisation JIT qui supprime la répartition dynamique : J'ai modifié le benchmark mentionné dans la question originale pour instancier une classe ou l'autre de façon aléatoire au moment de l'exécution. Puisque l'instanciation se produit après la compilation et après le chargement de l'assemblage/JIT, il n'y a aucun moyen d'éviter le dynamic dispatch dans les deux cas :
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 ---
Si cela intéresse quelqu'un, voici comment mon Visual C++ 2010 présente une instance d'une classe qui multiplie les héritages d'autres classes :
Code :
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;
}
};
Débogueur :
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) *
Les pointeurs de tables virtuelles multiples sont clairement visibles, et sizeof(C) == 8
(dans la version 32 bits).
Le...
C c;
std::cout << static_cast<IA*>(&c) << std::endl;
std::cout << static_cast<IB*>(&c) << std::endl;
..tirages...
0027F778
0027F77C
...indiquant que les pointeurs vers différentes interfaces au sein d'un même objet pointent en fait vers différentes parties de cet objet (c'est-à-dire qu'ils contiennent différentes adresses physiques).