La Performance du “direct” virtuel appel contre appel d'interface en C#
Cette référence semble montrer que l'appel d'une méthode virtuelle directement sur l'objet de référence est plus rapide que d'appeler sur la référence à l'interface de 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 C++ monde, je me serais attendu que deux de ces appels seraient mises en œuvre à l'identique (comme une simple table virtuelle de recherche) et ont le même rendement. Comment est-ce que C# mettre en œuvre les appels virtuels et quel est ce "plus" du travail qui, apparemment, se fait lors de l'appel par l'intermédiaire d'un interface?
--- EDIT ---
OK, réponses/commentaires je me suis tellement bien qu'il est un double pointeur de déréférencement pour les appels virtuel par le biais de l'interface à l'opposé d'un déréférencement pour les appels virtuel par l'objet.
Si quelqu'un pourrait s'il vous plaît expliquer pourquoi est-ce nécessaire? Quelle est la structure de la table virtuelle en C#? Est-il "à plat" (comme il est typique pour le C++) ou pas? Quelles ont été la conception des compromis qui ont été faits en langage C# design qui conduisent à cette situation? Je ne dis pas que c'est un "mauvais" le design, je suis simplement curieux de savoir pourquoi il a été nécessaire.
En un mot, je voudrais comprendre ce que mon outil n'sous le capot, donc je peux l'utiliser plus efficacement. Et j'apprécierais si je n'ai pas eu plus de "vous ne devriez pas savoir que" ou "utiliser une autre langue" types de réponses.
--- EDIT 2 ---
Juste pour rendre les choses claires, nous ne sommes pas à traiter avec un compilateur JIT d'optimisation qui supprime la répartition dynamique: j'ai modifié l'indice de référence mentionné dans la question d'origine d'instancier une classe ou de l'autre de manière aléatoire au moment de l'exécution. Depuis l'instanciation se passe après la compilation et l'assemblage de chargement/JITing, il n'y a aucun moyen d'éviter la distribution dynamique 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
}
}
--- EDITION 3 ---
Si quelqu'un est intéressé, voici comment mon Visual C++ 2010 définit une instance d'une classe qui se multiplient-hérite 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) *
Plusieurs virtuel tableau de pointeurs sont clairement visibles, et sizeof(C) == 8
(en 32 bits).
...
C c;
std::cout << static_cast<IA*>(&c) << std::endl;
std::cout << static_cast<IB*>(&c) << std::endl;
..estampes...
0027F778
0027F77C
...indiquant que des pointeurs vers des interfaces différentes au sein du même objet en fait montrer les différentes parties de l'objet (c'est à dire qu'ils contiennent différentes adresses physiques).
Une interface d'appel de méthode exige une double déréférencement de pointeur. C# devrais peut-être pas être la langue de votre choix si vous comptez nanosecondes. Le C et le C++ sont des langages qui sont optimisés pour que.
le fait que j'ai posé la question ne signifie pas que je suis "comptage de nanosecondes" sur tout projet concret. Je ne peux pas juste être curieux?
Votre question n'est pas de s'exprimer que d'intérêt.
Je vais reformuler: Dans les cas où le rendement est le facteur le plus critique, d'autres technologies devenir plus approprié. Fondamentalement, je suggère d'utiliser le bon outil pour le travail, et qu'il n'existe pas une telle chose comme un outil qui peut faire tout le meilleur.
OriginalL'auteur Branko Dimitrijevic | 2011-08-29
Vous devez vous connecter pour publier un commentaire.
Je pense que l'article à http://msdn.microsoft.com/en-us/magazine/cc163791.aspx répondra à vos questions. En particulier, voir la section Interface Vtable de la Carte et de la Carte d'Interface, et la section suivante sur Virtuel de l'Expédition.
Il est probablement possible de le compilateur JIT pour comprendre les choses et d'optimiser le code de votre cas simple. Mais pas dans le cas général.
Et
GetAFoo
est définie comme le retour d'unIFoo
, alors le compilateur JIT de ne pas être en mesure d'optimiser l'appel.Note de l'article ci-dessus est en Mai 2005 de MSDN magazine. Le lien actuel pour le téléchargement de l'émission est ici: download.microsoft.com/download/3/a/7/...
En plus de la réponse, voici le lien qui explique le raisonnement derrière pourquoi il n'y a pas d'héritage multiple .Net (à mon humble avis c'est la raison pour laquelle il n'existe qu'une table virtuelle pointeur) MSDN blog
Les liens sont morts. L'archive Tom lien est corrompu. Voici l'Internet Archive: Wayback Machine lien de droit après cette réponse a été publiée. Btw, le titre de l'article est "JIT et de l'Exécution: Percer .NET Framework lui-même pour Voir Comment le CLR Crée des Objets de l'Exécution".
OriginalL'auteur Jim Mischel
Voici ce que le dis-assemblée ressemble (Hans est correct):
Lors de l'accès à un objet par le biais d'une interface, l'interface de la fonction doit être "associé" à l'objet même de la fonction. Cela prend plus de temps et plus de code. Sauf si vous êtes l'écriture d'un compilateur, je ne voudrais pas passer beaucoup de temps sur ce sujet. Il y a 75 millions d'autres choses à apprendre.
La table virtuelle mécanisme en C++ est assez simple et utile de savoir que même si je ne suis pas "l'écriture d'un compilateur". Cela m'a surpris que le C# n'chose différemment et je suis curieuse, c'est tout. BTW, ce fut dans le contexte d'une autre question: [avantage Pratique de génériques vs interfaces en C#][1] [1]: stackoverflow.com/questions/7224675/...
Pourriez-vous nous donner plus d'explication sur POURQUOI il est de cette façon? Merci!
OriginalL'auteur Steve Wellens
J'ai essayé votre test et sur ma machine, dans un contexte particulier, le résultat est en fait l'inverse.
Je suis sous Windows 7 x64 et j'ai créé un Visual Studio 2010 projet d'Application Console dans lequel j'ai copié ton code. Si un compiler le projet en mode Debug et avec la plate-forme cible comme x86 la sortie sera la suivante:
En fait à chaque fois lors de l'exécution de l'application, il fournira des résultats légèrement différents, mais l'interface des appels sera toujours plus rapide. Je suppose que, puisque l'application est compilée en tant que x86, il sera exécuté par le système d'exploitation par le biais de WOW.
Pour une référence complète, ci-dessous sont les résultats pour le reste de la configuration de la compilation et de la cible combinaisons.
Libération mode et x86 cible
Appel Direct: 23.02
Par le biais de l'interface: 32.73
Debug mode et x64 cible
Appel Direct: 49.49
Par le biais de l'interface: 56.97
Libération mode et x64 cible
Appel Direct: 19.60
Par le biais de l'interface: à 26,45
Tous les tests ci-dessus ont été effectués .Net 4.0 en tant que plate-forme cible pour le compilateur. Lors de la commutation à 3,5 et de répéter les tests ci-dessus, les appels par le biais de l'interface ont été toujours plus de temps que les appels directs.
Donc, les tests ci-dessus plutôt compliquer les choses, puisqu'il semble que le comportement que vous repéré n'est pas toujours le cas.
À la fin, avec le risque de déplaire, je voudrais ajouter quelques réflexions. De nombreuses personnes ont ajouté des commentaires que les différences de performances sont assez petites et dans le monde réel de la programmation vous ne devriez pas vous soucier d'eux et je suis d'accord avec ce point de vue. Il y a deux raisons principales à cela.
La première et la plus annoncé l'une est que .Net a été de construire sur un niveau plus élevé afin de permettre aux développeurs de se concentrer sur l'augmentation des niveaux d'applications. Une base de données ou un service externe d'appel est à des milliers ou parfois des millions de fois plus lent que le virtuel appel de méthode. Avoir une bonne architecture de haut niveau et en se concentrant sur la grande performance de consommateurs apportera toujours de meilleurs résultats dans des applications modernes plutôt que d'éviter la double-pointeur-déréférence.
La deuxième et plus obscur, c'est que l' .Net de l'équipe par la construction d'un cadre d'un niveau supérieur, a en effet introduit une série de niveaux d'abstraction qui le juste à temps compilateur serait en mesure de l'utiliser pour faire des optimisations sur les différentes plates-formes. Le plus accès qu'ils voulaient donner à la sous-couches les plus de développeurs seraient en mesure d'optimiser votre site pour une plate-forme spécifique, mais le moins que l'exécution du compilateur serait en mesure de faire pour les autres. C'est la théorie, au moins, et c'est pourquoi les choses ne sont pas aussi bien documenté que dans C++ au sujet de cette question en particulier.
désolé de ne pas être en mesure d'apporter beaucoup de valeur à ce que vous êtes réellement à la recherche pour. Ma réponse est en fait plus une collection d'observations que j'ai voulu inclure le but de l'approche de plus haut niveau .Net, de sorte que d'autres personnes pour atteindre cette page vous bénéficiez également de ce point de vue sur les choses. D'autre part, les résultats mitigés basé sur le mode de compilation de montrer à quel variable .Net peuvent être liés à ces aspects.
OriginalL'auteur Florin Dumitrescu
Je pense que la fonction virtuelle pure affaire peut utiliser une simple fonction virtuelle table, comme toute classe dérivée de
Foo
la mise en œuvre deBar
voudrais juste changer le virtuel pointeur de fonction àBar
.D'autre part, l'appel d'une fonction d'interface IFoo:Bar ne pouvais pas faire une recherche de quelque chose comme
IFoo
virtuelle de la table de fonction, parce que chaque mise en œuvre deIFoo
n'a pas besoin de necceserely mettre en œuvre d'autres fonctions, ni les interfaces quiFoo
. Donc, le virtuel de la table de fonction position d'entrée pourBar
à partir d'un autreclass Fubar: IFoo
ne doit pas correspondre à la fonction virtuelle entrée de la table de la position deBar
dansclass Foo:IFoo
.Donc un appel de fonction virtuelle pure peut compter sur le même indice de la fonction pointeur à l'intérieur de la fonction virtuelle table dans chaque classe dérivée, alors que l'interface d'appel a pour consulter la cet indice en premier.
Je ne suis pas tout à fait sûr de savoir comment l'optimiser en général. Le réel de l'appel de la méthode dépend si pas évident de trouver un raccourci est détecté sur l'interface utilisée et les objets actuels de la classe. Cependant, l'interface ne peut pas fournir d'une manière générale, pour sélectionner un individu vtable dans n'importe quel objet, car il n'est pas fixe, la fente " pour une méthode d'interface pour être mis en œuvre. Votre exemple C++ pouvez sélectionner l'un des nombreux vtables de l'objet en raison de la fonte de la source et de la cible de la classe sont connus: la combinaison 'C' et 'IA' identifier de manière unique une vtable de l'intérieur C. Pour un pointeur d'interface comme "IFoo" qui n'est pas le cas.
Virtuel tableau de pointeurs dans un objet donné, toujours point de vtables qui sont spécifiques à l'objet implémentations des interfaces fournies. En d'autres termes, il n'y a pas une telle chose comme "interface" vtable - il ya seulement une "interface implémentée par la classe" vtable. Le pointeur d'interface indique toujours la "bonne" partie de l'objet, et par conséquent, le droit vtptr - si nous avons effectué notre jette correctement. Essentiellement les informations nécessaires à "sélection d'un individu vtable" est codé dans l'adresse physique contenue dans le pointeur.
OriginalL'auteur dronus
La règle générale est: les Classes sont rapides. Les Interfaces sont lents.
C'est une des raisons de la recommandation "de Construire des hiérarchies de classes et d'utiliser les interfaces pour les échanges intra-hiérarchie de comportement".
Pour les méthodes virtuelles, la différence peut sembler minime (de l'ordre de 10%). Mais pour les non-virtuel, de méthodes et de champs, la différence est énorme. Considérons ce programme.
De sortie:
Voulais juste ajouter de la matière à ce sujet, il n'y a pas beaucoup de discussions à propos de.
OriginalL'auteur Johan Nilsson