Fonctions virtuelles et performances - C ++

125

Dans la conception de ma classe, j'utilise beaucoup de classes abstraites et de fonctions virtuelles. J'avais le sentiment que les fonctions virtuelles affectaient les performances. Est-ce vrai? Mais je pense que cette différence de performance n'est pas perceptible et il semble que je fasse une optimisation prématurée. Droite?

Navaneeth KN
la source
Selon ma réponse, je suggère de fermer ceci en double de stackoverflow.com/questions/113830
Suma
duplication possible de la pénalité
Bo Persson
2
Si vous faites du calcul haute performance et du calcul des nombres, n'utilisez aucune virtualité au cœur du calcul: cela tue définitivement toutes les performances et empêche les optimisations à la compilation. Pour l'initialisation ou la finalisation du programme, ce n'est pas important. Lorsque vous travaillez avec des interfaces, vous pouvez utiliser la virtualité comme vous le souhaitez.
Vincent

Réponses:

90

Une bonne règle de base est:

Ce n'est pas un problème de performances jusqu'à ce que vous puissiez le prouver.

L'utilisation de fonctions virtuelles aura un très léger effet sur les performances, mais il est peu probable que cela affecte les performances globales de votre application. Les algorithmes et les E / S sont de meilleurs endroits pour rechercher des améliorations de performances.

Un excellent article qui parle des fonctions virtuelles (et plus) est les pointeurs de fonction de membre et les délégués C ++ les plus rapides possibles .

Greg Hewgill
la source
Qu'en est-il des fonctions virtuelles pures? Ont-ils une incidence sur les performances? Je me demande simplement comme il semble qu'ils sont là simplement pour appliquer la mise en œuvre.
thomthom
2
@thomthom: Correct, il n'y a pas de différence de performances entre les fonctions virtuelles pures et virtuelles ordinaires.
Greg Hewgill
168

Votre question m'a rendu curieux, alors je suis allé de l'avant et j'ai exécuté quelques timings sur le processeur PowerPC 3GHz dans l'ordre avec lequel nous travaillons. Le test que j'ai effectué était de créer une simple classe vectorielle 4d avec des fonctions get / set

class TestVec 
{
    float x,y,z,w; 
public:
    float GetX() { return x; }
    float SetX(float to) { return x=to; }  // and so on for the other three 
}

Ensuite, j'ai mis en place trois tableaux contenant chacun 1024 de ces vecteurs (assez petits pour tenir dans L1) et j'ai exécuté une boucle qui les a ajoutés les uns aux autres (Ax = Bx + Cx) 1000 fois. J'ai couru cela avec les fonctions définies comme inline, virtualet les appels de fonctions régulières. Voici les résultats:

  • en ligne: 8 ms (0,65 ns par appel)
  • direct: 68 ms (5,53 ns par appel)
  • virtuel: 160 ms (13 ns par appel)

Ainsi, dans ce cas (où tout tient dans le cache), les appels de fonction virtuelle étaient environ 20 fois plus lents que les appels en ligne. Mais qu'est-ce que cela signifie vraiment? Chaque voyage dans la boucle provoquait exactement 3 * 4 * 1024 = 12,288des appels de fonction (1024 vecteurs fois quatre composants multipliés par trois appels par ajout), donc ces temps représentent 1000 * 12,288 = 12,288,000des appels de fonction. La boucle virtuelle prenait 92 ms de plus que la boucle directe, de sorte que la surcharge supplémentaire par appel était de 7 nanosecondes par fonction.

De cela, je conclus: oui , les fonctions virtuelles sont beaucoup plus lentes que les fonctions directes, et non , à moins que vous ne prévoyiez de les appeler dix millions de fois par seconde, cela n'a pas d'importance.

Voir aussi: comparaison de l'assemblage généré.

Crashworks
la source
Mais s'ils sont appelés plusieurs fois, ils peuvent souvent être moins chers que lorsqu'ils ne sont appelés qu'une seule fois. Voir mon blog non pertinent: phresnel.org/blog , les articles intitulés «Fonctions virtuelles considérées comme non nuisibles», mais bien sûr cela dépend de la complexité de vos chemins de codes
Sebastian Mach
22
Mon test mesure un petit ensemble de fonctions virtuelles appelées à plusieurs reprises. Votre article de blog suppose que le coût en temps du code peut être mesuré en comptant les opérations, mais ce n'est pas toujours vrai; le coût majeur d'un vfunc sur les processeurs modernes est la bulle de pipeline causée par une erreur de branche.
Crashworks
10
ce serait une excellente référence pour gcc LTO (Link Time Optimization); essayez de compiler à nouveau avec lto activé: gcc.gnu.org/wiki/LinkTimeOptimization et voyez ce qui se passe avec le facteur 20x
lurscher
1
Si une classe a une fonction virtuelle et une fonction en ligne, les performances de la méthode non virtuelle seront-elles également affectées? Simplement par la nature de la classe virtuelle?
thomthom
4
@thomthom Non, virtuel / non virtuel est un attribut par fonction. Une fonction ne doit être définie via vtable que si elle est marquée comme virtuelle ou si elle remplace une classe de base qui l'a comme virtuelle. Vous verrez souvent des classes qui ont un groupe de fonctions virtuelles pour l'interface publique, puis beaucoup d'accesseurs en ligne et ainsi de suite. (Techniquement, c'est spécifique à l'implémentation et un compilateur pourrait utiliser des ponteurs virtuels même pour les fonctions marquées `` en ligne '', mais une personne qui a écrit un tel compilateur serait insensée.)
Crashworks
42

Lorsque Objective-C (où toutes les méthodes sont virtuelles) est le langage principal de l'iPhone et que le freakin ' Java est le langage principal pour Android, je pense qu'il est assez sûr d'utiliser les fonctions virtuelles C ++ sur nos tours dual-core 3 GHz.

Mandrin
la source
4
Je ne suis pas sûr que l'iPhone soit un bon exemple de code performant: youtube.com/watch?v=Pdk2cJpSXLg
Crashworks
13
@Crashworks: L'iPhone n'est pas du tout un exemple de code. C'est un exemple de matériel - en particulier de matériel lent , ce que je voulais dire ici. Si ces langages réputés «lents» sont assez bons pour du matériel sous-alimenté, les fonctions virtuelles ne seront pas un énorme problème.
Chuck
52
L'iPhone fonctionne sur un processeur ARM. Les processeurs ARM utilisés pour iOS sont conçus pour une faible MHz et une faible consommation d'énergie. Il n'y a pas de silicium pour la prédiction de branchement sur le CPU et par conséquent aucune surcharge de performance de la prédiction de branchement manque lors des appels de fonction virtuelle. De plus, le MHz pour le matériel iOS est suffisamment bas pour qu'un manque de cache ne bloque pas le processeur pendant 300 cycles d'horloge pendant qu'il récupère les données de la RAM. Les erreurs de cache sont moins importantes à des MHz inférieurs. En bref, il n'y a pas de surcharge liée à l'utilisation des fonctions virtuelles sur les appareils iOS, mais il s'agit d'un problème matériel et ne s'applique pas aux processeurs des ordinateurs de bureau.
HaltingState
4
En tant que programmeur Java de longue date nouvellement entré en C ++, je voudrais ajouter que le compilateur JIT et l'optimiseur d'exécution de Java ont la capacité de compiler, prédire et même incorporer certaines fonctions au moment de l'exécution après un nombre prédéfini de boucles. Cependant, je ne suis pas sûr que C ++ ait une telle fonctionnalité au moment de la compilation et de la liaison car il manque de modèle d'appel d'exécution. Ainsi, en C ++, nous devrons peut-être être légèrement plus prudents.
Alex Suo
@AlexSuo Je ne suis pas sûr de votre point? Étant compilé, C ++ bien sûr ne peut pas optimiser en fonction de ce qui pourrait se produire au moment de l'exécution, donc la prédiction, etc. devrait être faite par le processeur lui-même ... mais les bons compilateurs C ++ (si demandé) se donnent beaucoup de mal pour optimiser les fonctions et les boucles bien avant Durée.
underscore_d
34

Dans les applications très performantes (comme les jeux vidéo), un appel de fonction virtuelle peut être trop lent. Avec le matériel moderne, le plus gros problème de performances est le manque de cache. Si les données ne sont pas dans le cache, il peut s'écouler des centaines de cycles avant qu'elles ne soient disponibles.

Un appel de fonction normal peut générer un échec du cache d'instructions lorsque le processeur récupère la première instruction de la nouvelle fonction et qu'elle n'est pas dans le cache.

Un appel de fonction virtuelle doit d'abord charger le pointeur vtable à partir de l'objet. Cela peut entraîner un échec du cache de données. Ensuite, il charge le pointeur de fonction de la vtable, ce qui peut entraîner un autre échec du cache de données. Ensuite, il appelle la fonction qui peut entraîner un échec du cache d'instructions comme une fonction non virtuelle.

Dans de nombreux cas, deux échecs de cache supplémentaires ne sont pas un problème, mais dans une boucle serrée sur le code critique de performances, cela peut réduire considérablement les performances.

Mark James
la source
6
Bien sûr, mais tout code (ou vtable) appelé à plusieurs reprises à partir d'une boucle serrée souffrira (bien sûr) rarement de manque de cache. En outre, le pointeur vtable est généralement dans la même ligne de cache que les autres données de l'objet auxquelles la méthode appelée accédera, si souvent nous parlons d'un seul manque de cache supplémentaire.
Qwertie
5
@Qwertie Je ne pense pas que ce soit vraiment nécessaire. Le corps de la boucle (si plus grand que le cache L1) pourrait "retirer" le pointeur de la vtable, le pointeur de la fonction et l'itération ultérieure devraient attendre l'accès au cache L2 (ou plus) à chaque itération
Ghita
30

À partir de la page 44 du manuel "Optimizing Software in C ++" d' Agner Fog :

Le temps nécessaire pour appeler une fonction membre virtuelle est de quelques cycles d'horloge de plus qu'il n'en faut pour appeler une fonction membre non virtuelle, à condition que l'instruction d'appel de fonction appelle toujours la même version de la fonction virtuelle. Si la version change, vous obtiendrez une pénalité pour erreur de prédiction de 10 à 30 cycles d'horloge. Les règles de prédiction et de mauvaise prédiction des appels de fonction virtuelle sont les mêmes que pour les instructions switch ...

Boojum
la source
Merci pour cette référence. Les manuels d'optimisation d'Agner Fog sont la référence en matière d'utilisation optimale du matériel.
Arto Bendiken
Sur la base de mes souvenirs et d'une recherche rapide - stackoverflow.com/questions/17061967/c-switch-and-jump-tables - je doute que cela soit toujours vrai pour switch. Avec des casevaleurs totalement arbitraires , bien sûr. Mais si tous les cases sont consécutifs, un compilateur pourrait être en mesure d'optimiser cela en une table de saut (ah, qui me rappelle les bons vieux jours du Z80), qui devrait être (faute d'un meilleur terme) à temps constant. Non pas que je recommande d'essayer de remplacer les vfuncs par switch, ce qui est ridicule. ;)
underscore_d
7

absolument. C'était un problème à l'époque où les ordinateurs fonctionnaient à 100Mhz, car chaque appel de méthode nécessitait une recherche sur la vtable avant d'être appelé. Mais aujourd'hui ... sur un processeur 3Ghz qui a un cache de 1er niveau avec plus de mémoire que mon premier ordinateur? Pas du tout. Allouer de la mémoire à partir de la RAM principale vous coûtera plus de temps que si toutes vos fonctions étaient virtuelles.

C'est comme le bon vieux temps où les gens disaient que la programmation structurée était lente parce que tout le code était divisé en fonctions, chaque fonction nécessitait des allocations de pile et un appel de fonction!

La seule fois où je penserais même à prendre la peine de considérer l'impact sur les performances d'une fonction virtuelle, c'est si elle était très largement utilisée et instanciée dans un code basé sur un modèle qui s'est retrouvé dans tout. Même dans ce cas, je n'y consacrerais pas trop d'efforts!

PS pense à d'autres langages «faciles à utiliser» - toutes leurs méthodes sont virtuelles sous les couvertures et elles ne rampent pas de nos jours.

gbjbaanb
la source
4
Eh bien, même aujourd'hui, il est important d'éviter les appels de fonction pour les applications à haute performance. La différence est que les compilateurs actuels intègrent de manière fiable de petites fonctions afin de ne pas subir de pénalités de vitesse pour l'écriture de petites fonctions. En ce qui concerne les fonctions virtuelles, les processeurs intelligents peuvent effectuer une prédiction intelligente de branche sur eux. Le fait que les vieux ordinateurs étaient plus lents, je pense, n'est pas vraiment le problème - oui, ils étaient beaucoup plus lents, mais à l'époque nous le savions, alors nous leur avons donné des charges de travail beaucoup plus réduites. En 1992, si nous jouions un MP3, nous savions que nous devrons peut-être consacrer plus de la moitié du processeur à cette tâche.
Qwertie
6
mp3 date de 1995. en 92, nous en avions à peine 386, aucun moyen de lire un mp3, et 50% du temps du processeur suppose un bon système d'exploitation multi-tâches, un processus inactif et un planificateur préemptif. Rien de tout cela n'existait sur le marché de consommation à l'époque. c'était à 100% à partir du moment où l'alimentation était allumée, fin de l'histoire.
v.oddou
7

Il y a un autre critère de performance en plus du temps d'exécution. Une Vtable prend également de l'espace mémoire et, dans certains cas, peut être évitée: ATL utilise une " liaison dynamique simulée " à la compilation avec des modèlespour obtenir l'effet du «polymorphisme statique», ce qui est assez difficile à expliquer; vous passez essentiellement la classe dérivée en tant que paramètre à un modèle de classe de base, de sorte qu'au moment de la compilation, la classe de base "sait" quelle est sa classe dérivée dans chaque instance. Ne vous permettra pas de stocker plusieurs classes dérivées différentes dans une collection de types de base (c'est le polymorphisme à l'exécution) mais d'un sens statique, si vous voulez créer une classe Y identique à une classe de modèle préexistante X qui a le hooks pour ce genre de surcharge, il vous suffit de remplacer les méthodes qui vous intéressent, puis vous obtenez les méthodes de base de la classe X sans avoir à avoir une vtable.

Dans les classes avec de grandes empreintes mémoire, le coût d'un seul pointeur vtable n'est pas beaucoup, mais certaines des classes ATL dans COM sont très petites, et cela vaut les économies de vtable si le cas de polymorphisme d'exécution ne se produira jamais.

Voir aussi cette autre question SO .

En passant, voici un article que j'ai trouvé qui parle des aspects de performance du temps CPU.

Jason S
la source
1
C'est ce qu'on appelle le polymorphisme paramétrique
tjysdsg
4

Oui, vous avez raison et si vous êtes curieux de connaître le coût de l'appel de fonction virtuelle, vous pourriez trouver ce post intéressant.

Serge
la source
1
L'article lié ne considère pas une partie très importante de l'appel virtuel, et c'est une erreur de prédiction possible de la branche.
Suma
4

La seule façon dont je peux voir qu'une fonction virtuelle deviendra un problème de performances est si de nombreuses fonctions virtuelles sont appelées dans une boucle serrée, et si et seulement si elles provoquent une erreur de page ou une autre opération de mémoire «lourde».

Bien que, comme d'autres l'ont dit, cela ne sera pratiquement jamais un problème pour vous dans la vraie vie. Et si vous pensez que c'est le cas, exécutez un profileur, effectuez des tests et vérifiez si cela pose vraiment un problème avant d'essayer de «annuler la conception» de votre code pour améliorer les performances.

Daemin
la source
2
appeler quoi que ce soit dans une boucle serrée est susceptible de garder tout ce code et ces données à chaud dans le cache ...
Greg Rogers
2
Oui, mais si cette boucle droite itère dans une liste d'objets, chaque objet pourrait potentiellement appeler une fonction virtuelle à une adresse différente via le même appel de fonction.
Daemin le
3

Lorsque la méthode de classe n'est pas virtuelle, le compilateur effectue généralement l'in-line. Au contraire, lorsque vous utilisez un pointeur vers une classe avec une fonction virtuelle, l'adresse réelle ne sera connue qu'au moment de l'exécution.

Ceci est bien illustré par le test, décalage horaire ~ 700% (!):

#include <time.h>

class Direct
{
public:
    int Perform(int &ia) { return ++ia; }
};

class AbstrBase
{
public:
    virtual int Perform(int &ia)=0;
};

class Derived: public AbstrBase
{
public:
    virtual int Perform(int &ia) { return ++ia; }
};


int main(int argc, char* argv[])
{
    Direct *pdir, dir;
    pdir = &dir;

    int ia=0;
    double start = clock();
    while( pdir->Perform(ia) );
    double end = clock();
    printf( "Direct %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    Derived drv;
    AbstrBase *ab = &drv;

    ia=0;
    start = clock();
    while( ab->Perform(ia) );
    end = clock();
    printf( "Virtual: %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    return 0;
}

L'impact de l'appel de fonction virtuelle dépend fortement de la situation. S'il y a peu d'appels et une quantité importante de travail à l'intérieur de la fonction - cela pourrait être négligeable.

Ou, quand il s'agit d'un appel virtuel utilisé à plusieurs reprises, tout en effectuant une opération simple - cela peut être très important.

Evgueny Sedov
la source
4
Un appel de fonction virtuelle coûte cher par rapport à ++ia. Et alors?
Bo Persson
2

J'ai fait des allers-retours sur cela au moins 20 fois sur mon projet particulier. Bien qu'il puisse y avoir de grands gains en termes de réutilisation du code, de clarté, de maintenabilité et de lisibilité, en revanche, les performances sont toujours valables. exister avec des fonctions virtuelles.

La performance sera-t-elle perceptible sur un ordinateur portable / bureau / tablette moderne ... probablement pas! Cependant, dans certains cas avec des systèmes embarqués, la baisse des performances peut être le facteur déterminant de l'inefficacité de votre code, en particulier si la fonction virtuelle est appelée maintes et maintes fois en boucle.

Voici un article daté qui analyse les meilleures pratiques pour C / C ++ dans le contexte des systèmes embarqués: http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf

Pour conclure: c'est au programmeur de comprendre les avantages / inconvénients de l'utilisation d'une certaine construction par rapport à une autre. À moins que vous ne soyez axé sur les performances, vous ne vous souciez probablement pas de l'atteinte des performances et devriez utiliser toutes les astuces OO en C ++ pour rendre votre code aussi utilisable que possible.

C'estPete
la source
2

D'après mon expérience, le principal élément pertinent est la capacité à intégrer une fonction. Si vous avez des besoins en performances / optimisation qui imposent qu'une fonction doit être intégrée, vous ne pouvez pas rendre la fonction virtuelle car cela empêcherait cela. Sinon, vous ne remarquerez probablement pas la différence.


la source
1

Une chose à noter est que ceci:

boolean contains(A element) {
    for (A current: this)
        if (element.equals(current))
            return true;
    return false;
}

peut être plus rapide que cela:

boolean contains(A element) {
    for (A current: this)
        if (current.equals(equals))
            return true;
    return false;
}

En effet, la première méthode n'appelle qu'une seule fonction tandis que la seconde peut appeler de nombreuses fonctions différentes. Cela s'applique à toute fonction virtuelle dans n'importe quelle langue.

Je dis "peut" parce que cela dépend du compilateur, du cache, etc.

nikdeapen
la source
0

La pénalité de performance liée à l'utilisation de fonctions virtuelles ne peut jamais surpasser les avantages que vous obtenez au niveau de la conception. On suppose qu'un appel à une fonction virtuelle serait 25% moins efficace qu'un appel direct à une fonction statique. C'est parce qu'il existe un niveau d'indirection via le VMT. Cependant, le temps nécessaire pour effectuer l'appel est normalement très petit par rapport au temps nécessaire à l'exécution réelle de votre fonction, de sorte que le coût total des performances sera négligeable, en particulier avec les performances actuelles du matériel. De plus, le compilateur peut parfois optimiser et voir qu'aucun appel virtuel n'est nécessaire et le compiler en un appel statique. Alors ne vous inquiétez pas, utilisez autant de fonctions virtuelles et de classes abstraites que nécessaire.


la source
2
jamais jamais, quelle que soit la taille de l'ordinateur cible?
zumalifeguard
J'aurais pu être d'accord si vous aviez formulé cela comme The performance penalty of using virtual functions can sometimes be so insignificant that it is completely outweighed by the advantages you get at the design level.La principale différence est de dire sometimes, non never.
underscore_d
-1

Je me suis toujours posé la question, d'autant plus que - il y a quelques années - j'ai également fait un tel test comparant les horaires d'un appel de méthode de membre standard avec un appel virtuel et j'étais vraiment en colère contre les résultats à ce moment-là, ayant des appels virtuels vides étant 8 fois plus lent que les non-virtuels.

Aujourd'hui, je devais décider d'utiliser ou non une fonction virtuelle pour allouer plus de mémoire dans ma classe de tampon, dans une application très critique pour les performances, alors j'ai cherché sur Google (et vous ai trouvé), et à la fin, j'ai refait le test.

// g++ -std=c++0x -o perf perf.cpp -lrt
#include <typeinfo>    // typeid
#include <cstdio>      // printf
#include <cstdlib>     // atoll
#include <ctime>       // clock_gettime

struct Virtual { virtual int call() { return 42; } }; 
struct Inline { inline int call() { return 42; } }; 
struct Normal { int call(); };
int Normal::call() { return 42; }

template<typename T>
void test(unsigned long long count) {
    std::printf("Timing function calls of '%s' %llu times ...\n", typeid(T).name(), count);

    timespec t0, t1;
    clock_gettime(CLOCK_REALTIME, &t0);

    T test;
    while (count--) test.call();

    clock_gettime(CLOCK_REALTIME, &t1);
    t1.tv_sec -= t0.tv_sec;
    t1.tv_nsec = t1.tv_nsec > t0.tv_nsec
        ? t1.tv_nsec - t0.tv_nsec
        : 1000000000lu - t0.tv_nsec;

    std::printf(" -- result: %d sec %ld nsec\n", t1.tv_sec, t1.tv_nsec);
}

template<typename T, typename Ua, typename... Un>
void test(unsigned long long count) {
    test<T>(count);
    test<Ua, Un...>(count);
}

int main(int argc, const char* argv[]) {
    test<Inline, Normal, Virtual>(argc == 2 ? atoll(argv[1]) : 10000000000llu);
    return 0;
}

Et j'ai été vraiment surpris que cela - en fait - n'ait vraiment plus d'importance. Bien qu'il soit logique d'avoir des inlines plus rapides que les non-virtuels, et qu'ils soient plus rapides que les virtuels, il s'agit souvent de la charge de l'ordinateur dans son ensemble, de savoir si votre cache contient les données nécessaires ou non, et même si vous pourrez peut-être optimiser au niveau du cache, je pense que cela devrait être fait par les développeurs du compilateur plus que par les développeurs d'applications.

christianparpart
la source
12
Je pense qu'il est fort probable que votre compilateur puisse dire que l'appel de fonction virtuelle dans votre code ne peut appeler que Virtual :: call. Dans ce cas, il peut simplement l'intégrer. Il n'y a également rien qui empêche le compilateur d'insérer Normal :: call même si vous ne l'avez pas demandé. Je pense donc qu'il est tout à fait possible que vous obteniez les mêmes temps pour les 3 opérations car le compilateur génère un code identique pour elles.
Bjarke H.Roune