О какой ветке в деструкторе сообщает gcov?

Когда я использую 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 ответа

Решение

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

Я предполагаю, что, поскольку в вашем случае уничтожение происходит для нединамического объекта, динамическая ветвь не берется. Попробуйте добавить new и затем delete объект класса 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, имеет множество бессмысленных недоступных веток, подобных этой.

В деструкторе 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>
[...]

Проблема с деструктором все еще существует для 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 ..." для генерации покрытия, как описано здесь.

Другие вопросы по тегам