Performances des appels virtuels «directs» et des appels d’interface en C #

Ce test semble montrer que l’appel d’une méthode virtuelle directement sur la référence d’object est plus rapide que l’appel à la référence à l’interface implémentée par cet object.

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 C ++, je m’attendrais à ce que ces deux appels soient implémentés de manière identique (comme une simple recherche de table virtuelle) et aient les mêmes performances. Comment C # implémente-t-il les appels virtuels et quel est ce travail “supplémentaire” qui est apparemment réalisé lors de l’appel via une interface?

— MODIFIER —

OK, les réponses / commentaires que j’ai obtenus jusqu’à présent impliquent qu’il existe un déréférencement à double pointeur pour les appels virtuels via l’interface et une seule déréférence pour les appels virtuels via les objects.

Alors, pourriez-vous s’il vous plaît que quelqu’un explique pourquoi est-ce nécessaire? Quelle est la structure de la table virtuelle en C #? Est-ce que c’est “plat” (comme c’est typique pour C ++) ou pas? Quels ont été les compromis de conception qui ont été faits dans la conception du langage C # pour y parvenir? Je ne dis pas que c’est un “mauvais” design, je suis simplement curieux de savoir pourquoi c’était nécessaire.

En un mot, j’aimerais comprendre ce que fait mon outil sous le capot pour pouvoir l’utiliser plus efficacement. Et j’apprécierais que je n’obtienne plus “vous ne devriez pas savoir cela” ou “utilisez une autre langue”.

— EDIT 2 —

Pour que ce soit clair, nous ne traitons pas ici de compilateur d’optimisation JIT qui supprime le dispatch dynamic: j’ai modifié le benchmark mentionné dans la question d’origine pour instancier une classe ou l’autre au hasard au moment de l’exécution. L’instanciation se produisant après la compilation et après le chargement de l’assemblage / JIT, il n’y a aucun moyen d’éviter une répartition dynamic 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(ssortingng[] 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 quelqu’un est intéressé, voici comment Visual C ++ 2010 présente une instance d’une classe qui hérite de plusieurs 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) * 

Plusieurs pointeurs de table virtuelle sont clairement visibles et sizeof(C) == 8 (en version 32 bits).

Le…

 C c; std::cout << static_cast(&c) << std::endl; std::cout << static_cast(&c) << std::endl; 

..prints …

 0027F778 0027F77C 

… indiquant que des pointeurs vers différentes interfaces du même object pointent effectivement vers différentes parties de cet object (c.-à-d. qu’ils contiennent des adresses physiques différentes).

Je pense que l’article de http://msdn.microsoft.com/en-us/magazine/cc163791.aspx répondra à vos questions. En particulier, reportez-vous à la section Carte de l’interface Vtable et carte d’interface , et à la section suivante sur Virtual Dispatch.

Il est probablement possible que le compilateur JIT calcule et optimise le code pour votre cas simple. Mais pas dans le cas général.

 IFoo f2 = GetAFoo(); 

Et GetAFoo est défini comme renvoyant un IFoo , alors le compilateur JIT ne pourrait pas optimiser l’appel.

Voici à quoi ressemble le désassemblage (Hans a raison):

  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] 

J’ai essayé votre test et sur ma machine, dans un contexte particulier, le résultat est en réalité l’inverse.

J’utilise Windows 7 x64 et j’ai créé un projet Visual Studio 2010 Console Application dans lequel j’ai copié votre code. Si le projet est compilé en mode Debug et que la cible de la plate-forme est x86, le résultat sera le suivant:

Appel direct: 48.38
Via l’interface: 42.43

En fait, à chaque fois que l’application exécute l’application, elle fournit des résultats légèrement différents, mais les appels d’interface seront toujours plus rapides. Je suppose que puisque l’application est compilée en x86, elle sera exécutée par le système d’exploitation via WOW.

Pour une référence complète, vous trouverez ci-dessous les résultats pour le rest de la configuration de compilation et des combinaisons de cibles.

Mode de publication et cible x86
Appel direct: 23.02
À travers l’interface: 32.73

Mode de débogage et cible x64
Appel direct: 49.49
À travers l’interface: 56.97

Mode de publication et cible x64
Appel direct: 19.60
À travers l’interface: 26.45

Tous les tests ci-dessus ont été réalisés avec .Net 4.0 comme plate-forme cible du compilateur. Lors du passage à 3.5 et de la répétition des tests ci-dessus, les appels via l’interface étaient toujours plus longs que les appels directs.

Donc, les tests ci-dessus compliquent plutôt les choses, car il semble que le comportement que vous avez repéré ne se produit pas toujours.

Au final, avec le risque de vous contrarier, j’aimerais append quelques reflections. Beaucoup de personnes ont ajouté que les différences de performances sont assez faibles et que dans la vraie vie, vous ne devriez pas vous en soucier et je suis d’accord avec ce sharepoint vue. Il y a deux raisons principales à cela.

Le premier et le plus annoncé est que .Net a été construit à un niveau supérieur afin de permettre aux développeurs de se concentrer sur les niveaux d’applications les plus élevés. Une firebase database ou un appel de service externe est des milliers ou parfois des millions de fois plus lent que l’appel à la méthode virtuelle. Avoir une bonne architecture de haut niveau et se concentrer sur les gros consommateurs de performance apportera toujours de meilleurs résultats dans les applications modernes plutôt que d’éviter les déréférences à double pointeur.

La seconde, plus obscure, est que l’équipe .Net, en construisant la structure à un niveau supérieur, a en fait introduit une série de niveaux d’abstraction que le compilateur juste-à-temps pourrait utiliser pour optimiser des plates-formes différentes. Plus ils donneraient d’access aux sous-couches, plus les développeurs seraient en mesure de les optimiser pour une plate-forme spécifique, mais moins le compilateur d’exécution serait capable de le faire pour les autres. C’est la théorie du moins et c’est pourquoi les choses ne sont pas aussi bien documentées qu’en C ++ en ce qui concerne cette question particulière.

Je pense que le cas de la fonction virtuelle pure peut utiliser une simple table de fonction virtuelle, car toute classe dérivée de Foo implémentant Bar ne ferait que changer le pointeur de fonction virtuel en Bar .

Par contre, appeler une fonction d’interface IFoo: Bar ne peut pas faire une recherche sur quelque chose comme la table de fonction virtuelle d’ IFoo , car chaque implémentation d’ IFoo n’a pas besoin d’implémenter d’autres fonctions ni interfaces que Foo fait. Donc, la position d’entrée de la table de fonction virtuelle pour Bar d’une autre class Fubar: IFoo ne doit pas correspondre à la position d’entrée de la table de fonction virtuelle de Bar dans la class Foo:IFoo .

Ainsi, un appel de fonction virtuelle pure peut s’appuyer sur le même index du pointeur de fonction à l’intérieur de la table de fonction virtuelle dans chaque classe dérivée, tandis que l’appel d’interface doit d’abord rechercher cet index.

La règle générale est la suivante: Les classes sont rapides. Les interfaces sont lentes.

C’est l’une des raisons de la recommandation “Construire des hiérarchies avec des classes et utiliser des interfaces pour un comportement intra-hiérarchique”.

Pour les méthodes virtuelles, la différence peut être faible (comme 10%). Mais pour les méthodes et les champs non virtuels, la différence est énorme. Considérez ce programme.

 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(ssortingng[] 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(); } } } 

Sortie:

 a.Counter: 1560 ia.Counter: 4587