28 votes

Qu'est-ce qui empêche g++ d'éliminer les std::array temporaires non utilisés en cours d'exécution ?

#include <array>
#include <cassert>

class P {
  public:
    P() : _value(nullptr) {}
    ~P() { delete _value; }

  private:
   char *_value;
};

void foo() {
  if(std::array<P, 4>().size() != 4)
    assert(false);
}

La fonction foo() crée un tableau temporaire pour vérifier que la taille correspond à ce que le programmeur attendait. Avec -O1 ou plus, g++ calcule le assert n'échouera pas et l'appel à __assert_fail est supprimé du code généré. Mais g++ génère toujours du code pour construire et détruire le tableau maintenant inutilisé.

g++ -std=c++11 -O3 [4.8.2]:

0000000000000000 <_Z3foov>:1
   0:       55                      push   %rbp1
   1:       66 0f ef c0             pxor   %xmm0,%xmm01
   5:       53                      push   %rbx1
   6:       48 83 ec 28             sub    $0x28,%rsp1
   a:       66 0f 7f 04 24          movdqa %xmm0,(%rsp)1
   f:       48 8d 5c 24 20          lea    0x20(%rsp),%rbx1
  14:       48 89 e5                mov    %rsp,%rbp1
  17:       66 0f 7f 44 24 10       movdqa %xmm0,0x10(%rsp)1
  1d:       0f 1f 00                nopl   (%rax)1
  20:       48 83 eb 08             sub    $0x8,%rbx1
  24:       48 8b 3b                mov    (%rbx),%rdi1
  27:       e8 00 00 00 00          callq  2c <_Z3foov+0x2c>1
  2c:       48 39 eb                cmp    %rbp,%rbx1
  2f:       75 ef                   jne    20 <_Z3foov+0x20>1
  31:       48 83 c4 28             add    $0x28,%rsp1
  35:       5b                      pop    %rbx1
  36:       5d                      pop    %rbp1
  37:       c3                      retq   1

clang, d'autre part, supprime tout le code sauf l'instruction de retour.

clang -std=c++11 -O3:

0000000000000000 <_Z3foov>:1
   0:       c3                      retq   1

Juste de la malchance avec g++ ou y a-t-il une raison à cette différence ?

5voto

user3125280 Points 1817

Tout d'abord, excellente question.

EDIT :

Je n'ai pas vraiment lu votre code correctement la première fois. Il y a un appel externe important dans votre code . C'est dans cette instruction e8 00 00 00 00 callq 2c <_Z3foov+0x2c>1 . Il n'appelle pas l'adresse avant lui-même - au lieu de cela, sa cible sera remplacée au moment de la liaison. C'est ainsi que la liaison fonctionne - le fichier elf dira "telle ou telle instruction sera résolue vers cette cible au moment de la liaison". L'assembleur n'a pas été répertorié complètement et nous ne connaissons donc pas l'adresse de cet appel. On peut supposer que c'est delete _value dans le code, cependant libstdc++ (avec delet, etc) est lié dynamiquement par défaut. Vous pouvez changer cela en utilisant mes drapeaux de compilateur, ou faire votre liste dans gdb (c'est-à-dire après l'édition de liens).

Revenons à la réponse :

C'est un cas particulier où les programmeurs de gcc ont fait le choix de ne pas optimiser. La raison est que les opérateurs new et delete sont marqués comme des 'symboles faibles' et l'éditeur de liens cherchera des alternatives, choisissant un utilisateur fourni ou revenant en arrière si aucun n'est trouvé.

Ici est une discussion sur le raisonnement qui sous-tend cette démarche. Ces opérateurs sont conçus pour être globalement remplaçables, et la liaison faible est une solution.

L'établissement de liens statiques ne change pas cela, car free et malloc dans la glibc peuvent toujours être modifiés au moment de la liaison.

Liaison statique, ou utilisation de l'optimisation du temps de liaison devrait utiliser la suppression intégrée, cependant, dans ce cas Si, au lieu de cela, vous établissez un lien statique, la chance est toujours manquée. Dans votre exemple original, cependant, cette chance n'existe pas.

Je compile ça :

#include <array>
#include <cassert>
#include <cstdlib>

void * operator new(std::size_t n) throw(std::bad_alloc)
{
  return malloc(n);
}
void operator delete(void * p) throw()
{
if(p != nullptr)
  free(p);
}

class P {
  public:
    P() : _value(nullptr) {}
    ~P() { delete _value; }

  private:
   char *_value;
};

void foo() {
  if(std::array<P, 4>().size() != 4)
    assert(false);
}

int main(){
foo();
}

avec ceci ; g++ -std=c++11 -O3 -static -Wa,-alh test.cpp -o test

liens vers libstdc++/glibc statiquement Il devrait donc savoir ce que sont free et malloc, mais il ne réalise pas tout à fait que foo est trivial.

Cependant, nm -gC test | grep free produit le symbole faible __free_hook pour laquelle j'ai trouvé une explication aquí . Ainsi, le comportement de free et malloc (et donc, de l'opérateur new et delete) est en fait toujours modifiable à l'adresse temps de liaison lors de la compilation avec gcc. C'est ce qui empêche l'optimisation - fâcheusement, l'option -fno-weak laisse ces symboles là.

Le paragraphe ci-dessus est vrai, mais il est le résultat de l'optimisation manquée, et non une raison d'éviter l'optimisation.

Avec l'optimisation du temps de liaison, il est possible d'amener gcc à faire cette optimisation, mais vous devez d'abord construire tout le reste avec -flto, y compris libstdc++.

Voici ce que gcc fait de mieux pour moi lors de la construction statique de l'exemple :

Dump of assembler code for function _Z3foov:
0x08048ef0 <+0>:    push   %esi
0x08048ef1 <+1>:    push   %ebx
0x08048ef2 <+2>:    sub    $0x24,%esp
0x08048ef5 <+5>:    movl   $0x0,0x10(%esp)
0x08048efd <+13>:   lea    0x20(%esp),%ebx
0x08048f01 <+17>:   movl   $0x0,0x14(%esp)
0x08048f09 <+25>:   lea    0x10(%esp),%esi
0x08048f0d <+29>:   movl   $0x0,0x18(%esp)
0x08048f15 <+37>:   movl   $0x0,0x1c(%esp)
0x08048f1d <+45>:   lea    0x0(%esi),%esi
0x08048f20 <+48>:   sub    $0x4,%ebx
0x08048f23 <+51>:   mov    (%ebx),%eax
0x08048f25 <+53>:   test   %eax,%eax
0x08048f27 <+55>:   je     0x8048f31 <_Z3foov+65>
0x08048f29 <+57>:   mov    %eax,(%esp)
0x08048f2c <+60>:   call   0x804dea0 <free>
0x08048f31 <+65>:   cmp    %esi,%ebx
0x08048f33 <+67>:   jne    0x8048f20 <_Z3foov+48>
0x08048f35 <+69>:   add    $0x24,%esp
0x08048f38 <+72>:   pop    %ebx
0x08048f39 <+73>:   pop    %esi
0x08048f3a <+74>:   ret  

L'appel à la gratuité ne va nulle part.

Si cela pose un problème, optimisez-le vous-même. Les modèles rendent cela facile (en supposant que std::array est passé en tant qu'argument de modèle, sinon pourquoi vérifier sa taille() ?).

#include <array>
#include <cassert>

class P {
  public:
    P() : _value(nullptr) {}
    ~P() { delete _value; }

  private:
   char *_value;
};

void foo() {
  if(std::tuple_size<std::array<P, 4> >::value != 4)
    assert(false);
}

int main(){
foo();
}

On peut faire en sorte que le code échoue silencieusement si std::array<P, 4> est un vecteur ou quelque chose comme ça, et se rabattre sur votre méthode de construction par défaut.

nm -C test sorties W operator new(unsigned int, void*) quand j'ai ajouté #include <new> Les autres sont spéciaux, et leurs cibles éventuelles résident dans libstdc++ (encore une fois, ils sont liés dynamiquement par défaut).

0voto

franklin Points 131

Parce qu'il pourrait y avoir des effets secondaires dans le constructeur de std::array . Cependant, comme g++ et clang n'utilisent pas la même bibliothèque standard (libstdc++ pour g++ et libc++ pour clang).

Pour la question de savoir pourquoi clang supprime le code, peut-être que dans la libc++ le std::array Le constructeur de l'utilisateur est en ligne, de sorte que l'optimiseur peut voir qu'il n'y a pas d'effets secondaires, et qu'il faut donc conserver le code.

0voto

David Greene Points 109

Cela peut se produire pour un certain nombre de raisons. Peut-être que gcc n'inline pas aussi bien que clang pour une raison quelconque. Augmenter les boutons d'inlining de gcc pourrait aider. Ou peut-être y a-t-il quelque chose d'autre qui se passe, comme un problème d'aliasing que gcc ne peut pas résoudre pour une raison quelconque. Il est impossible de savoir sans tracer à travers gcc pour découvrir les détails.

En fin de compte, il s'agit simplement de deux compilateurs différents effectuant des transformations de code différentes. gcc pourrait être amélioré pour couvrir ce cas.

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