48 votes

Pourquoi GCC n'optimise-t-il pas la suppression des pointeurs nuls en C ++?

Envisager un programme simple:

int main() {
  int* ptr = nullptr;
  delete ptr;
}

Avec GCC (7.2), il y a un call les instructions relatives à l' operator delete dans le programme résultant. Avec Clang et compilateurs Intel, il n'y a pas de telles instructions, le pointeur null suppression est complètement optimisé out (-O2 dans tous les cas). Vous pouvez tester ici: https://godbolt.org/g/JmdoJi.

Je me demande si cette optimisation peut être en quelque sorte tourné avec GCC? (Mes plus large de la motivation vient d'un problème de coutume swap vs std::swap pour les types de mobiliers, d'où la suppression de pointeurs null peut représenter une perte de performance dans le deuxième cas; voir https://stackoverflow.com/a/45689282/580083 pour plus de détails.)

Mise à JOUR

Pour préciser ma motivation pour la question: Si j'utilise juste delete ptr; sans if (ptr) garde un opérateur d'assignation de déplacement et un destructeur d'une classe, alors std::swap avec des objets de la classe des rendements de 3 call instructions avec GCC. Cela pourrait être un considérable sur les performances, par exemple, lors du tri d'un tableau de ces objets.

En outre, je peux écrire if (ptr) delete ptr; partout, mais me demande, si cela ne peut pas être une pénalité sur les performances ainsi, depuis delete expression des besoins pour vérifier l' ptr ainsi. Mais, ici, je suppose, les compilateurs génèrent un seul chèque uniquement.

Aussi, j'aime vraiment la possibilité de faire appel delete sans le garde et c'était une surprise pour moi, qu'il pourrait donner des résultats différents (de la performance) des résultats.

Mise à JOUR

J'ai juste fait un simple indice de référence, à savoir le tri des objets, qui invoquent delete dans leur mouvement à l'opérateur d'assignation et destructeur. La source est ici: https://godbolt.org/g/7zGUvo

Temps de fonctionnement de std::sort mesurée avec GCC 7.1 -O2 drapeau sur Xeon E2680v3:

Il y a un bug dans les liens de code, il compare les pointeurs, pas de relever les valeurs. Corrigé les résultats sont comme suit:

  1. sans if garde: 17.6 [s] de 40,8 [s],
  2. avec if garde: 10.6 [s] de 31,5 [s],
  3. avec if de la garde et de la coutume swap: 10.4 [s] de 31,3 [s].

Ces résultats ont été absolument cohérent à travers de nombreuses pistes avec un minimum de déviation. La différence de performances entre les deux premiers cas est significatif et je ne dirais pas que c'est une certaine "extrêmement rares cas de coin" comme code.

29voto

Matt McNabb Points 14273

En fonction de C++14 [expr.supprimer]/7:

Si la valeur de l'opérande de la suppression de l'expression n'est pas une valeur de pointeur null, alors:

  • [ ...omis... ]

Sinon, il n'est pas précisé si la fonction de libération sera appelée.

Donc, les deux compilateurs ne sont conformes à la norme, parce que c'est pas spécifié si operator delete est appelé à la suppression d'un pointeur null.

Notez que le godbolt en ligne compilateur juste compile le fichier source sans liaison. Ainsi, le compilateur, à ce stade, doit permettre la possibilité qu' operator delete sera remplacé par un autre fichier source.

Comme déjà spéculé dans une autre réponse -- gcc peut-être la pêche à la ligne constante de leur comportement dans le cas d'un remplacement, operator delete; cette mise en œuvre serait de dire que quelqu'un peut surcharger cette fonction à des fins de débogage et de pause sur toutes les invocations de l' delete d'expression, même quand il s'agit de la suppression d'un pointeur null.

Mise à JOUR: suppression de la spéculation que cela pourrait ne pas être un problème pratique, puisque OP fourni des repères, en montrant qu'il en fait.

7voto

Swift Points 6

Standard de fait les états lorsque l'allocation et la libération des fonctions doit être appelé et où ils en ont pas. Cette clause (@ n4296)

La bibliothèque fournit des définitions par défaut pour la répartition mondiale et libération de la mémoire de fonctions. Certains globale de l'allocation et de désallocation les fonctions sont remplaçables (18.6.1). Un programme C++ doit fournir, à plus qu'une définition d'un remplaçable d'allocation et de désallocation fonction. Une telle définition de la fonction remplace la version par défaut fournis dans la bibliothèque (17.6.4.6). L'affectation suivante et libération de la mémoire de fonctions (18.6) sont déclarées implicitement dans la portée globale dans chaque unité de traduction d'un programme.

sans doute serait la principale raison pour laquelle les appels de fonction ne sont pas omis arbitraire. Si elles l'étaient, le remplacement de leur mise en œuvre de la bibliothèque de cause incohérente fonction du programme compilé.

Dans la première variante (supprimer l'objet), la valeur de l'opérande de supprimer peut être une valeur de pointeur null, un pointeur vers un non-objet array créé par une précédente nouvelle expression, ou un pointeur vers un sous-objet (1.8) représentant une classe de base d'un tel objet (article 10). Si pas, le comportement est indéfini.

Si l'argument donné à une fonction de libération dans la norme la bibliothèque est un pointeur qui n'est pas la valeur de pointeur null (4.10), la la fonction de libération est de libérer le stockage référencé par le pointeur, rendu invalide tous les pointeurs se référant à une partie de la libéré de stockage. Indirection par l'intermédiaire d'un pointeur invalide valeur et en passant un pointeur non valide de la valeur à une fonction de libération ont un comportement indéfini. Toute autre utilisation d'une valeur de pointeur non valide a la mise en œuvre définies par le comportement.

...

Si la valeur de l'opérande de la suppression de l'expression n'est pas un null la valeur du pointeur, puis

  • Si l'appel d'allocation pour la nouvelle expression pour la suppression de l'objet n'a pas été omis et l'allocation n'a pas été prolongé (5.3.4), la suppression de l'expression doit appeler une fonction de libération (3.7.4.2). La valeur renvoyée par l'appel d'allocation de la nouvelle expression doit être passé comme premier argument de la fonction de libération.

  • Sinon, si l'allocation a été étendu ou a été fourni par l'extension de l'allocation de l'autre newexpression, et la suppression d'expression pour tous les autres pointeur de la valeur produite par une nouvelle expression, qui avait de stockage fourni par la nouvelle expression a été évaluée, la suppression de l'expression doit convoquer une la fonction de libération. La valeur renvoyée par l'appel d'allocation de la nouvelle-expression doit être passé comme premier argument la fonction de libération.

    • Sinon, la suppression de l'expression ne vais pas appeler une fonction de libération

Sinon, il n'est pas précisé si la fonction de libération sera appelée.

Norme indique ce qui doit être fait si le pointeur n'est PAS null. Ce qui implique que supprimer dans ce cas est noop, mais à quelle fin, n'est pas spécifié.

7voto

Richard Hodges Points 1972

C'est un problème QOI. Clang élide effectivement le test:

https://godbolt.org/g/nBSykD

 main:                                   # @main
        xor     eax, eax
        ret
 

6voto

Peter Cordes Points 1375

Il est toujours plus sûr (de justesse) pour laisser votre appel de programme operator delete avec un nullptr.

Pour les performances, il est très rare que le fait d'avoir généré par le compilateur asm fait faire un test et de la branche conditionnelle pour passer d'un appel à l' operator delete sera une victoire. (Vous pouvez vous aider de gcc optimiser loin au moment de la compilation nullptr suppression sans l'ajout d'un moment de l'exécution, bien que; voir ci-dessous).

Tout d'abord, plus de code de taille en dehors d'un vrai hot-spot augmente la pression sur le L1I cache, et même la plus petite décodé-uop cache sur les Processeurs x86 qui ont un (Intel banque nationale, de la famille, AMD Ryzen).

Deuxièmement, extra branches conditionnelles utilisation des entrées dans la branche de la prédiction des caches (BTB = Direction de la Cible Tampon et ainsi de suite). Selon le CPU, même une branche qui n'a jamais pris le peut aggraver les prédictions pour les autres branches si c'alias dans le BTB. (Sur les autres, telle une branche n'est jamais une entrée dans le BTB, pour enregistrer les entrées pour les branches où la statique par défaut de prévision de l'automne est exacte.) Voir https://xania.org/201602/bpu-part-one.

Si nullptr est rare dans un chemin de code, puis sur la moyenne de la vérification et de la direction générale pour éviter l' call se termine avec votre programme de passer plus de temps sur la coche de la case à sauve.

Si le profilage montre que vous avez un hot-spot qui comprend un delete, et de l'instrumentation / enregistrement montre qu'elle a souvent fait des appels delete avec un nullptr, alors il vaut la peine d'essayer
if (ptr) delete ptr; au lieu de simplement en delete ptr;

Direction de la prévision pourrait avoir plus de chance qu'un site d'appel que, pour la direction, à l'intérieur d' operator delete, surtout si il y a une corrélation avec d'autres à proximité des branches. (Apparemment moderne Mru, ne regardez pas seulement chaque branche dans l'isolation). C'est au sommet d'enregistrement les inconditionnels call dans la fonction de bibliothèque (avec un jmp de la PLT relevé, de la liaison dynamique de surcharge sur Unix/Linux).


Si vous êtes à la vérification de la valeur null pour toute autre raison, alors il peut faire sens pour mettre la delete à l'intérieur de la non-nulle de la branche de votre code.

Vous pouvez éviter delete des appels dans les cas où gcc peut prouver (après inlining) qu'un pointeur est null, mais sans en faire un moment de l'exécution si ce n':

static inline bool 
is_compiletime_null(const void *ptr) {
#ifdef   __GNUC__
    // __builtin_constant_p(ptr) is false even for nullptr,
    // but the checking the result of booleanizing works.
    return __builtin_constant_p(!ptr) && !ptr;
#else
    return false;
#endif
}

Il renverra toujours false avec clang, car il évalue __builtin_constant_p avant l'in-lining. Mais depuis clang déjà saute delete des appels lorsqu'il peut prouver un pointeur est null, vous n'en avez pas besoin.

Cela pourrait effectivement aider en std::move des cas, et vous pouvez l'utiliser en toute sécurité n'importe où avec (en théorie) pas de performance à la baisse. J'ai toujours compile if(true) ou if(false), il est donc très différente de if(ptr), qui est de nature à entraîner l'exécution de la branche, car le compilateur ne peut probablement pas prouver que le pointeur n'est pas null dans la plupart des cas, soit. (Un déréférencement peut, cependant, car un nul deref serait UB, et les compilateurs modernes optimisé basé sur l'hypothèse que le code ne contient aucune UB).

Vous pourriez en faire une macro pour éviter les ballonnements non-optimisé construit (et donc qu'il serait "travail" sans avoir à inline premier). Vous pouvez utiliser un système GNU C déclaration expression afin d'éviter une double évaluation de la macro arg (voir les exemples pour GNU C min() et max()). Pour la solution de repli pour les compilateurs sans extensions GNU, vous pouvez écrire ((ptr), false) ou quelque chose à évaluer l'arg une fois que les effets secondaires, tout en produisant un false résultat.

Démonstration: asm de gcc6.3 -O3 sur la Godbolt compilateur explorer

void foo(int *ptr) {
    if (!is_compiletime_null(ptr))
        delete ptr;
}

    # compiles to a tailcall of operator delete
    jmp     operator delete(void*)


void bar() {
    foo(nullptr);
}

    # optimizes out the delete
    rep ret

Il compile correctement avec MSVC (également sur le compilateur explorer le lien), mais avec le test retourne toujours false, bar() est:

    # MSVC doesn't support GNU C extensions, and doesn't skip nullptr deletes itself
    mov      edx, 4
    xor      ecx, ecx
    jmp      ??3@YAXPEAX_K@Z      ; operator delete

Intéressant de noter que MSVC de l' operator delete prend la taille de l'objet en tant que une fonction arg (mov edx, 4), mais gcc/Linux/libstdc++ code passe le pointeur.


Connexes: j'ai trouvé ce blog, à l'aide de C11 (pas de C++11) _Generic pour essayer de portably faire quelque chose comme __builtin_constant_p de pointeur null contrôles à l'intérieur des initialiseurs statiques.

2voto

stefan bachert Points 4698

Je pense que, le compilateur n'a pas de connaissances sur "supprimer", surtout que "supprimer null" est un NOOP.

Vous pouvez écrire explicite, de sorte que le compilateur n'a pas besoin d'impliquer des connaissances sur supprimer.

AVERTISSEMENT: je ne recommande pas ce que la mise en œuvre générale. L'exemple suivant devrait montrer, comment vous avez pu "convaincre" limitée compilateur de code de suppression de toute façon que très spéciaux et limités programme

int main() {
 int* ptr = nullptr;

 if (ptr != nullptr) {
    delete ptr;
 }
}

Où je me souviens bien, il y a un moyen de remplacer "supprimer" avec une fonction propre. Et dans le cas d'une optimisation par le compilateur s'est mal passé.


@RichardHodges: Pourquoi devrait-il être un de-optimisation quand on donne le compilateur l'astuce pour supprimer un appel?

supprimer la valeur null est en général un NOOP (pas d'opération). Cependant, puisqu'il est possible de remplacer ou écraser supprimer il n'y a pas de garantie pour tous les cas.

Donc, c'est au compilateur de savoir et de décider d'utiliser les connaissances de supprimer la valeur null peut toujours supprimé. il existe de bons arguments pour à la fois choises

Cependant, le compilateur est toujours permis d'enlever le code mort, ce "if (false) {...}" ou "si (nullptr != nullptr) {...}"

Donc un compilateur va supprimer du code mort et ensuite lors de l'utilisation explicite de la vérification, on dirait

int main() {
 int* ptr = nullptr;

 // dead code    if (ptr != nullptr) {
 //        delete ptr;
 //     }
}

S'il vous plaît dites-moi, où est-il un de-optimisation?

J'appelle ma proposition d'un style défensif de codage, mais pas de l'optimisation

Si quelqu'un peut plaider, que, maintenant, la non-nullptr sera à l'origine de deux fois la vérification sur nullptr, je dois répondre

  1. Désolé, ce n'était pas la question d'origine
  2. si le compilateur sait à propos de les supprimer, surtout que supprimer la valeur null est une noop, que le compilateur pourrait supprimer l'extérieur si que ce soit. Cependant, je ne voudrais pas que les compilateurs pour être précis

@Peter Cordes: je suis d'accord gardiennage avec un si n'est pas une optimisation générale de la règle. Cependant, l'optimisation n'était PAS la question de l'ouvre-porte. La question était de savoir pourquoi certains compilateur ne pas elimate le supprimer dans un très court, les non-sens du programme. J'ai montré un moyen de rendre le compilateur de l'éliminer de toute façon.

Si une situation vous arrive, comme dans ce programme court, probablement quelque chose d'autre a tort. En général, je voudrais essayer d'éviter de new/delete (malloc/free) car les appels sont plutôt cher. Si possible, je préfère utiliser la pile (auto).

Quand je prends un coup d'oeil à l'intervalle documenté cas réel, je dirais, la classe X est conçu de mal, entraînant de mauvaises performances et trop de mémoire. (https://godbolt.org/g/7zGUvo)

Au lieu de

class X {
  int* i_;
  public:
  ...

en fait la conception d'

class X {
  int i;
  bool valid;
  public:
  ...

ou plus tôt, je vous prie de le sens de tri vide/objets non valides. À la fin, je tiens à vous débarrasser de "valide", trop.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X