41 votes

Quelle est la branche du destructeur rapportée par gcov?

Lorsque j'utilise gcov pour mesurer la couverture de test de code de C++, il rapporte des succursales dans des destructeurs.

struct Foo
{
    virtual ~Foo()
    {
    }
};

int main (int argc, char* argv[])
{
    Foo f;
}

Quand je lance gcov de la direction générale des probabilités activé (-b) - je obtenir la sortie suivante.

$ gcov /home/epronk/src/lcov-1.9/example/example.gcda -o /home/epronk/src/lcov-1.9/example -b
File 'example.cpp'
Lines executed:100.00% of 6
Branches executed:100.00% of 2
Taken at least once:50.00% of 2
Calls executed:40.00% of 5
example.cpp:creating 'example.cpp.gcov'

La partie qui me dérange, c'est la "Pris au moins une fois:50.00% de 2".

Le générés .gcov fichier donne plus de détails.

$ cat example.cpp.gcov | c++filt
        -:    0:Source:example.cpp
        -:    0:Graph:/home/epronk/src/lcov-1.9/example/example.gcno
        -:    0:Data:/home/epronk/src/lcov-1.9/example/example.gcda
        -:    0:Runs:1
        -:    0:Programs:1
        -:    1:struct Foo
function Foo::Foo() called 1 returned 100% blocks executed 100%
        1:    2:{
function Foo::~Foo() called 1 returned 100% blocks executed 75%
function Foo::~Foo() called 0 returned 0% blocks executed 0%
        1:    3:    virtual ~Foo()
        1:    4:    {
        1:    5:    }
branch  0 taken 0% (fallthrough)
branch  1 taken 100%
call    2 never executed
call    3 never executed
call    4 never executed
        -:    6:};
        -:    7:
function main called 1 returned 100% blocks executed 100%
        1:    8:int main (int argc, char* argv[])
        -:    9:{
        1:   10:    Foo f;
call    0 returned 100%
call    1 returned 100%
        -:   11:}

Remarquez la ligne "branche 0 prises 0% (fallthrough)".

Quelles sont les causes de cette branche et que dois-je faire dans le code pour obtenir 100% d'ici?

  • g++ (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2
  • gcov (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2

63voto

AndreyT Points 139512

Dans un cadre typique de la mise en œuvre, le destructeur est habituellement de deux branches: l'une pour les non-dynamique de la destruction des objets, un autre pour la dynamique de la destruction des objets. La sélection d'une branche spécifique est effectuée par un caché paramètre booléen passé pour le destructeur par l'appelant. Elle est habituellement transmise par l'intermédiaire d'un registre, soit 0 ou 1.

Je suppose que, puisque dans votre cas, la destruction est un non-objet dynamique, la dynamique de la branche n'est pas pris. Essayez d'ajouter un new-ed et puis, delete-ed objet de la classe Foo et la seconde branche devrait devenir ainsi.

La raison de cette ramification est nécessaire est enracinée dans la spécification du langage C++. Lorsqu'une classe définit ses propres operator delete, le choix du type operator delete d'appel est fait comme si c'était de l'intérieur de la classe destructeur. Le résultat final est que pour les classes avec un destructeur virtuel operator delete se comporte comme si elle était un virtuel de la fonction (malgré officiellement un statique membre de la classe).

De nombreux compilateurs implémenter ce comportement littéralement: le bon operator delete est appelée directement à partir de l'intérieur du destructeur de la mise en œuvre. Bien sûr, operator delete ne doit être appelée lors de la destruction d' dynamiquement les objets alloués (pas pour les locaux ou les objets statiques). Pour atteindre cet objectif, l'appel à l' operator delete est placé dans une branche contrôlée par le paramètre caché mentionnés ci-dessus.

Dans votre exemple, les choses semblent assez trivial. Je m'attends à de l'optimiseur de supprimer la ramification. Cependant, il semble qu'il en ait réussi à survivre à l'optimisation.


Voici un peu plus de recherche. Considérer ce code

#include <stdio.h>

struct A {
  void operator delete(void *) { scanf("11"); }
  virtual ~A() { printf("22"); }
};

struct B : A {
  void operator delete(void *) { scanf("33"); }
  virtual ~B() { printf("44"); }
};

int main() {
  A *a = new B;
  delete a;
} 

C'est la façon dont le code pour le destructeur de l' A va ressembler quand compilateur GCC 4.3.4 sous paramètres d'optimisation par défaut

__ZN1AD2Ev:                      ; destructor A::~A  
LFB8:
        pushl   %ebp
LCFI8:
        movl    %esp, %ebp
LCFI9:
        subl    $8, %esp
LCFI10:
        movl    8(%ebp), %eax
        movl    $__ZTV1A+8, (%eax)
        movl    $LC1, (%esp)     ; LC1 is "22"
        call    _printf
        movl    $0, %eax         ; <------ Note this
        testb   %al, %al         ; <------ 
        je      L10              ; <------ 
        movl    8(%ebp), %eax    ; <------ 
        movl    %eax, (%esp)     ; <------ 
        call    __ZN1AdlEPv      ; <------ calling `A::operator delete`
L10:
        leave
        ret

(Le destructeur d' B est un peu plus compliqué, c'est pourquoi j'utilise A ici comme un exemple. Mais aussi loin que la ramification en question, destructeur d' B t-il de la même manière).

Cependant, dès la fin de ce destructeur le code généré contient une autre version de l'destructeur pour la même classe d' A, ce qui semble exactement le même, à l'exception de l' movl $0, %eax instruction est remplacé par movl $1, %eax enseignement.

__ZN1AD0Ev:                      ; another destructor A::~A       
LFB10:
        pushl   %ebp
LCFI13:
        movl    %esp, %ebp
LCFI14:
        subl    $8, %esp
LCFI15:
        movl    8(%ebp), %eax
        movl    $__ZTV1A+8, (%eax)
        movl    $LC1, (%esp)     ; LC1 is "22"
        call    _printf
        movl    $1, %eax         ; <------ See the difference?
        testb   %al, %al         ; <------
        je      L14              ; <------
        movl    8(%ebp), %eax    ; <------
        movl    %eax, (%esp)     ; <------
        call    __ZN1AdlEPv      ; <------ calling `A::operator delete`
L14:
        leave
        ret

Remarque les blocs de code je étiquetés avec des flèches. C'est exactement ce dont je parlais. Inscrivez al sert en tant que paramètre caché. Cette "pseudo-branche" est censé soit appeler ou d'ignorer l'appel d' operator delete conformément à la valeur de al. Cependant, dans la première version du destructeur de ce paramètre est codé en dur dans le corps, comme toujours, 0, tandis que dans le second il est codé en dur, comme toujours, 1.

Classe B a également deux versions de l'destructeur généré pour lui. On se retrouve donc avec 4 distinctif destructeurs dans le programme compilé: deux destructeurs pour chaque classe.

Je peux deviner que, au début du compilateur interne de la pensée dans les termes d'un seul "paramétrée" destructeur (qui fonctionne exactement comme je l'ai décrit au-dessus de la pause). Et puis il a décidé de scinder le paramétrée destructeur en deux indépendants non paramétrée versions: l'une pour la codé en dur la valeur de paramètre 0 (non-dynamique destructeur) et l'autre pour la codé en dur la valeur de paramètre 1 (dynamique destructeur). En non-mode optimisé, il n'a, littéralement, par l'affectation de la valeur effective du paramètre à l'intérieur du corps de la fonction et en laissant toutes les branches totalement intact. Cela est acceptable dans la non optimisation de code, je suppose. Et c'est exactement ce que vous avez à faire.

En d'autres termes, la réponse à votre question est la suivante: Il est impossible de faire le compilateur de prendre toutes les branches dans ce cas. Il n'y a pas moyen d'atteindre une couverture de 100%. Certaines de ces branches sont "morts". C'est juste que l'approche de la génération de non-code optimisé est plutôt "paresseux" et "lâche" dans cette version de GCC.

Il y a peut être un moyen de prévenir la répartition des non-mode optimisé, je pense. Je n'en ai pas encore trouvé. Ou, très probablement, il ne peut pas être fait. Les anciennes versions de GCC utilisée vrai paramétrée destructeurs. Peut-être que dans cette version de GCC ils ont décidé de passer à deux destructeur approche et tout en le faisant, ils "réutilisés" le générateur de code dans de tels rapide et sale, attend l'optimiseur pour nettoyer les branches inutiles.

Lorsque vous avez activé l'optimisation de la compilation avec GCC ne pourra pas se permettre un tel luxe inutile de ramification dans le code final. Vous devriez probablement essayer d'analyser un code optimisé. Non optimisé GCC-code généré a beaucoup de sens inaccessible branches comme celui-ci.

7voto

Adam Mitz Points 4540

Dans le destructeur, GCC a généré un saut conditionnel pour une condition qui ne peut jamais être vraie (% al n'est pas nul, puisqu'il lui a été attribué un 1):

 [...]
  29:   b8 01 00 00 00          mov    $0x1,%eax
  2e:   84 c0                   test   %al,%al
  30:   74 30                   je     62 <_ZN3FooD0Ev+0x62>
[...]
 

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