О какой ветке в деструкторе сообщает gcov? - PullRequest
39 голосов
/ 26 августа 2011

Когда я использую gcov для измерения покрытия тестами кода C ++, он сообщает о ветвлениях в деструкторах.

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

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

Когда я запускаю gcov с включенными вероятностями ветвления (-b), я получаю следующий вывод:

$ 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'

Часть, которая беспокоит меня, это "Взятый хотя бы один раз: 50.00"% от 2 ".

Сгенерированный файл .gcov дает более подробную информацию.

$ 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:}

Обратите внимание на строку" Ветка 0 взята 0% (падение) ".

Чтовызывает эту ветку и что мне нужно сделать в коде, чтобы получить здесь 100%?

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

Ответы [ 3 ]

56 голосов
/ 26 августа 2011

В типичной реализации деструктор обычно имеет две ветви: одну для уничтожения нединамического объекта, другую для уничтожения динамического объекта.Выбор конкретной ветви выполняется через скрытый логический параметр, передаваемый деструктору вызывающей стороной.Обычно он передается через регистр как 0 или 1.

Я бы предположил, что, поскольку в вашем случае уничтожение происходит для нединамического объекта, динамическая ветвь не берется.Попробуйте добавить объект new -ed, а затем delete -ed класса Foo, и вторая ветвь также должна быть получена.

Причина, по которой это ветвление необходимо, коренится в спецификации C ++.язык.Когда некоторый класс определяет свой собственный operator delete, выбор конкретного operator delete для вызова выполняется так, как если бы он был найден внутри деструктора класса.Конечным результатом этого является то, что для классов с виртуальным деструктором operator delete ведет себя так, как если бы это была виртуальная функция (несмотря на то, что формально она является статическим членом класса).

Многие компиляторы реализуют это поведение буквально : правильный operator delete вызывается непосредственно из реализации деструктора.Конечно, operator delete следует вызывать только при уничтожении динамически выделенных объектов (не для локальных или статических объектов).Для этого вызов operator delete помещается в ветку, управляемую скрытым параметром, упомянутым выше.

В вашем примере все выглядит довольно тривиально.Я ожидаю, что оптимизатор удалит все ненужные ветвления.Однако, похоже, что каким-то образом ему удалось пережить оптимизацию.


Вот несколько дополнительных исследований.Рассмотрим этот код

#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;
} 

Так будет выглядеть код деструктора A при компиляции с GCC 4.3.4 при настройках оптимизации по умолчанию

__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

(деструкториз B немного сложнее, поэтому я использую A в качестве примера. Но что касается разветвления, деструктор из B делает то же самое).

Однако сразу после этого деструктора сгенерированный код содержит еще одну версию деструктора для того же класса A, который выглядит точно таким же , за исключением movl $0, %eaxинструкция заменена на movl $1, %eax инструкцию.

__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

Обратите внимание на кодовые блоки, которые я пометил стрелками.Это именно то, о чем я говорил.Регистр al служит этим скрытым параметром.Предполагается, что эта «псевдо-ветвь» либо вызывает, либо пропускает вызов на operator delete в соответствии со значением al.Однако в первой версии деструктора этот параметр жестко закодирован в теле как всегда 0, а во второй он жестко закодирован как всегда 1.

Класс B также имеет две версиидеструктора, созданного для него.Таким образом, мы получаем 4 отличительных деструктора в скомпилированной программе: два деструктора для каждого класса.

Я могу предположить, что в начале компилятор внутренне мыслил в терминах одного «параметризованного» деструктора (который работает точно так же, какЯ описал выше перерыв).Затем было принято решение разделить параметризованный деструктор на две независимые непараметрические версии: одна для значения параметра с жестким кодом 0 (нединамический деструктор), а другая для значения параметра с жесткой кодировкой 1 (динамический деструктор).В неоптимизированном режиме он делает это буквально, присваивая фактическое значение параметра в теле функции и оставляя все ветвления полностью нетронутыми.Я думаю, это приемлемо в неоптимизированном коде.И это именно то, с чем вы имеете дело.

Другими словами, ответ на ваш вопрос: Невозможно заставить компилятор взять все ветви в этом случае. Нет возможности достичь 100% покрытия. Некоторые из этих веток «мертвы». Просто в этой версии GCC подход к генерации неоптимизированного кода довольно «ленив» и «бесполезен».

Может быть, есть способ предотвратить раскол в неоптимизированном режиме, я думаю. Я просто еще не нашел это. Или, вполне возможно, это не может быть сделано. В старых версиях GCC использовались истинно параметризованные деструкторы. Возможно, в этой версии GCC они решили перейти на подход с двумя деструкторами, и, делая это, они так быстро и «грязно» использовали существующий генератор кода, ожидая, что оптимизатор очистит ненужные ветви.

Когда вы компилируете с включенной оптимизацией, GCC не допустит такой роскоши, как бесполезное ветвление в конечном коде. Возможно, вам следует попытаться проанализировать оптимизированный код. Неоптимизированный код, сгенерированный GCC, имеет множество бессмысленных недоступных веток, подобных этой.

7 голосов
/ 26 августа 2011

В деструкторе GCC сгенерировал условный переход для условия, которое никогда не может быть истинным (% al не равно нулю, поскольку ему просто присвоили 1):

[...]
  29:   b8 01 00 00 00          mov    $0x1,%eax
  2e:   84 c0                   test   %al,%al
  30:   74 30                   je     62 <_ZN3FooD0Ev+0x62>
[...]
0 голосов
/ 10 января 2018

Проблема с деструктором все еще существует для gcc версии 5.4.0, но, похоже, не существует для Clang.

Проверено с:

clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

Затем используйте «llvm-cov gcov ...» для генерации покрытия, как описано здесь .

...