web-dev-qa-db-fra.com

Fonctions virtuelles et performances - C ++

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

114
Navaneeth K N

Une bonne règle d'or est la suivante:

Ce n'est pas un problème de performances tant que vous ne pouvez pas le prouver.

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

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

86
Greg Hewgill

Votre question m'a rendu curieux, alors j'ai continué et j'ai exécuté quelques synchronisations sur le processeur PowerPC 3GHz dans lequel nous travaillons. Le test que j'ai exécuté consistait à créer une classe vectorielle 4d simple 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 configuré trois tableaux contenant chacun 1024 de ces vecteurs (suffisamment petits pour tenir dans L1) et j'ai exécuté une boucle qui les a ajoutés les uns aux autres (A.x = B.x + C.x) 1000 fois. J'ai exécuté cela avec les fonctions définies comme inline, virtual et les appels de fonction réguliers. 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)

Donc, 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 trajet dans la boucle a causé exactement 3 * 4 * 1024 = 12,288 appels de fonction (1024 vecteurs fois quatre composants fois trois appels par ajout), donc ces temps représentent 1000 * 12,288 = 12,288,000 appels de fonction. La boucle virtuelle a pris 92 ms de plus que la boucle directe, donc la surcharge supplémentaire par appel était de 7 nanosecondes par fonction.

J'en 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'assembly généré.

162
Crashworks

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

42
Chuck

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 d'être disponibles.

Un appel de fonction normal peut générer un échec de cache d'instructions lorsque le CPU 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 à partir de la table virtuelle, 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 étroite sur le code critique de performance, il peut réduire considérablement les performances.

34
Mark James

À partir de la page 44 de manuel "Optimisation des logiciels en C++" d'Agner Fog :

Le temps qu'il faut pour appeler une fonction membre virtuelle est de quelques cycles d'horloge de plus que 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é d'erreur de 10 à 30 cycles d'horloge. Les règles de prédiction et de mauvaise prévision des appels de fonction virtuelle sont les mêmes que pour les instructions de commutation ...

27
Boojum

absolument. C'était un problème de retour lorsque les ordinateurs fonctionnaient à 100 MHz, car chaque appel de méthode nécessitait une recherche sur la table virtuelle avant son 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 main RAM vous coûtera plus de temps que si toutes vos fonctions étaient virtuelles.

C'est comme les vieux jours 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 a été très largement utilisée et instanciée dans un code basé sur des modèles qui s'est retrouvé partout. Même alors, 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.

7
gbjbaanb

Il y a un autre critère de performance en plus du temps d'exécution. Une Vtable occupe également de l'espace mémoire, et dans certains cas, elle peut être évitée: ATL utilise le temps de compilation " liaison dynamique simulée " avec modèles pour obtenir l'effet de "statique polymorphisme ", ce qui est difficile à expliquer; vous passez essentiellement la classe dérivée en tant que paramètre à un modèle de classe de base, donc au moment de la compilation, la classe de base "sait" quelle est sa classe dérivée dans chaque instance. Ne vous permet pas de stocker plusieurs classes dérivées différentes dans une collection de types de base (c'est le polymorphisme d'exécution) mais d'un point de vue statique, si vous voulez créer une classe Y qui est identique à une classe de modèle X préexistante qui a le crochets pour ce type de substitution, 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 table virtuelle.

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 vtables si le cas du polymorphisme d'exécution ne se produira jamais.

Voir aussi cette autre SO question .

À propos, voici ne publication que j'ai trouvée qui parle des aspects de performance du temps CPU.

6
Jason S

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

4
Serge

La seule façon que je puisse voir qu'une fonction virtuelle deviendra un problème de performance est si de nombreuses fonctions virtuelles sont appelées dans une boucle étroite, et si et seulement si elles provoquent un défaut de page ou autre "lourd" "opération de mémoire à se produire.

Bien que, comme d'autres l'ont dit, cela ne deviendra 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 c'est vraiment un problème avant d'essayer de "annuler la conception" de votre code pour améliorer les performances.

3
Daemin

Lorsque la méthode de classe n'est pas virtuelle, le compilateur effectue généralement le doublage. Au contraire, lorsque vous utilisez le 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, différence de temps ~ 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, lorsqu'il s'agit d'un appel virtuel utilisé à plusieurs reprises à plusieurs reprises, tout en effectuant une opération simple - il peut être très important.

3
Evgueny Sedov

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

Les performances seront-elles visibles sur un ordinateur portable/bureau/tablette moderne ... probablement pas! Cependant, dans certains cas avec des systèmes embarqués, la perte de performances peut être le facteur déterminant de l'inefficacité de votre code, surtout si la fonction virtuelle est appelée maintes et maintes fois dans une 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 d'utiliser une certaine construction plutôt qu'une autre. À moins que vous ne soyez axé sur les performances, vous ne vous souciez probablement pas des performances et devriez utiliser toutes les astuces OO en C++ pour rendre votre code aussi utilisable que possible.

2
It'sPete

D'après mon expérience, la principale chose pertinente est la capacité à intégrer une fonction. Si vous avez des besoins de performances/d'optimisation qui dictent 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.

2
Hurkyl

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 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" car cela dépend du compilateur, du cache, etc.

1
nikdeapen

La pénalité de performance liée à l'utilisation de fonctions virtuelles ne peut jamais surestimer les avantages que vous obtenez au niveau de la conception. Soi-disant, un appel à une fonction virtuelle serait 25% moins efficace qu'un appel direct à une fonction statique. En effet, il existe un niveau d'indirection à travers le VMT. Cependant, le temps nécessaire pour effectuer l'appel est normalement très faible 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.

0
Koen