gcov报告的析构函数中的分支是什么?

What is the branch in the destructor reported by gcov?

本文关键字:是什么 分支 析构函数 报告 gcov      更新时间:2023-10-16

当我使用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:}

注意这行"branch 0 taken 0% (fallthrough)"

是什么导致了这个分支,我需要在代码中做什么才能在这里获得100% ?

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

在典型的实现中,析构函数通常有两个分支:一个用于非动态对象的析构,另一个用于动态对象的析构。特定分支的选择是通过调用者传递给析构函数的隐藏布尔参数来执行的。它通常以0或1的形式通过寄存器。

我猜,因为在你的情况下,销毁是针对非动态对象的,所以没有采取动态分支。尝试添加Foo类的new -ed和delete -ed对象,第二个分支也会被占用。

这个分支是必要的原因是植根于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的destructor也是这样做的。

然而,在这个析构函数之后,生成的代码包含了同一个类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作为该隐藏参数。这个"伪分支"应该根据al的值调用或跳过对operator delete的调用。然而,在析构函数的第一个版本中,该形参被硬编码为始终0,而在第二个版本中,它被硬编码为始终1

B也为它生成了两个版本的析构函数。因此,编译后的程序中有4个不同的析构函数:每个类有两个析构函数。

我可以猜测,一开始编译器内部考虑的是单个"参数化"析构函数(它的工作方式与我在上面的break描述的完全一样)。然后决定将参数化析构函数拆分为两个独立的非参数化版本:一个用于0的硬编码参数值(非动态析构函数),另一个用于1的硬编码参数值(动态析构函数)。在非优化模式下,它通过在函数体中分配实际的参数值并保持所有分支完全完整来执行此操作。我想这在非优化代码中是可以接受的。这就是你要处理的问题。

换句话说,你的问题的答案是:在这种情况下,不可能使编译器接受所有分支。没有办法达到100%的覆盖率。有些树枝已经"枯死"了。只是在这个版本的GCC中,生成非优化代码的方法相当"懒惰"answers"松散"。

我认为可能有一种方法可以防止非优化模式下的分裂。我只是还没找到。或者,很有可能,这是不可能做到的。旧版本的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…"来生成这里描述的覆盖率