gcov报告的析构函数中的分支是什么? [英] What is the branch in the destructor reported by gcov?

查看:1282
本文介绍了gcov报告的析构函数中的分支是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当我使用gcov来测量C ++代码的测试覆盖率时,它会在析构函数中报告分支。

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

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

当我运行gcov时启用分支概率输出。

  $ gcov /home/epronk/src/lcov-1.9/example/example.gcda -o / home / epronk /src/lcov-1.9/example -b 
文件'example.cpp'
执行的行:100.00%of 6
执行的分支:100.00%of 2
至少执行一次:50.00%of 2
执行的调用:40.00%of 5
example.cpp:创建'example.cpp.gcov'

困扰我的部分是至少一次:2的50.00%。



生成的.gcov文件提供更多详细信息。

  $ cat example.cpp.gcov | c ++ filt 
- :0:Source:example.cpp
- :0:Graph:/home/epronk/src/lcov-1.9/example/example.gcno
- :0 :数据:/home/epronk/src/lcov-1.9/example/example.gcda
- :0:运行:1
- :0:程序:1
- :1:struct Foo
函数Foo :: Foo()调用1返回100%块执行100%
1:2:{
函数Foo ::〜Foo()调用1返回100% %
函数Foo ::〜Foo()调用0返回0%块执行0%
1:3:virtual〜Foo()
1:4:{
1: 5:}
branch 0 taken 0%(fallthrough)
branch 1 taken 100%
call 2从未执行
调用3从未执行
调用4从未执行
- :6:};
- :7:
函数main调用1返回100%块执行100%
1:8:int main(int argc,char * argv [])
- :9 :{
1:10:Foo f;
call 0 returned 100%
call 1 returned 100%
- :11:}


b $ b

注意这一行branch 0 taken 0%(fallthrough)。



什么原因导致这个分支和我需要做什么在代码中




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

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


解决方案

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



我猜想,因为在你的情况下,销毁是一个非动态对象,动态分支不采取。尝试向 Foo -ed和删除



这种分支是必要的原因是根源于C ++语言的规范。当一些类定义自己的 operator delete 时,选择特定的操作符delete 从类内部析构函数中查找。最后的结果是,对于具有虚拟析构函数的操作符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:析构函数A ::〜A 
LFB8:
pushl%ebp
LCFI8:
movl%esp,%ebp
LCFI9:
subl $ 8,%esp
LCFI10:
movl 8(%ebp),%eax
movl $ __ ZTV1A + 8,(%eax)
movl $ LC1, LC1是22
call _printf
movl $ 0,%eax; < ------注意这个
testb%al,%al; < ------
je L10; < ------
movl 8(%ebp),%eax; < ------
movl%eax,(%esp); < ------
call __ZN1AdlEPv; < ------调用`A :: operator delete`
L10:
leave
ret

B 的析构函数有点复杂,这就是为什么我使用 A 这里作为一个例子,但就所讨论的分支来说, B 的析构函数也是这样)。



然而,在析构函数之后,生成的代码包含另一个版本的析构函数 A 除了 movl $ 0,%eax 指令替换为 movl $ 1,%eax / code>指令。

  __ ZN1AD0Ev:;另一个析构函数A ::〜A 
LFB10:
pushl%ebp
LCFI13:
movl%esp,%ebp
LCFI14:
subl $ 8, esp
LCFI15:
movl 8(%ebp),%eax
movl $ __ ZTV1A + 8,(%eax)
movl $ LC1, LC1是22
call _printf
movl $ 1,%eax; ------看到的区别?
testb%al,%al; < ------
je L14; < ------
movl 8(%ebp),%eax; < ------
movl%eax,(%esp); < ------
call __ZN1AdlEPv; < ------调用`A :: operator delete`
L14:
leave
ret

注意我用箭头标记的代码块。这正是我在说的。寄存器 al 用作该隐藏参数。这个伪分支应该根据 al 的值调用或跳过对 operator delete 的调用>。然而,在析构函数的第一个版本中,这个参数被硬编码到正文中,总是 0 ,而在第二个版本中它被硬编码为 1



B 也有两个版本的析构函数。因此,我们在编译的程序中最终得到4个不同的析构函数:每个类有两个析构函数。



我可以猜到, 参数化析构函数(其工作正如我上面描述的休息)。然后它决定将参数化析构函数拆分为两个独立的非参数化版本:一个用于硬编码参数值 0 (非动态析构函数),另一个用于硬编码参数值 1 (动态析构函数)。在非优化模式中,它通过在函数体内分配实际的参数值并且保留所有分支完全不变来实现。这是可以接受的非优化代码,我猜。这正是你正在处理的。



换句话说,你的问题的答案是:在这种情况下,编译器不可能采取所有的分支。没有办法实现100%的覆盖。这些分支中有一些是死的。只是在这个版本的GCC中生成非优化代码的方法相当懒惰和宽松。



我认为可能有一种方法可以防止非优化模式下的拆分。我只是还没有找到它。或者,很可能,它不能做。旧版本的GCC使用真正的参数化析构函数。也许在这个版本的GCC,他们决定切换到两个析构函数的方法,并在做它们重用现有的代码生成器这样一种快速和脏的方式,期望优化器清除无用的分支。 p>

当使用优化进行编译时,GCC不会允许自己在最终代码中使用无用的分支。你应该尝试分析优化的代码。非优化的GCC生成的代码有很多无意义的不可访问的分支,像这一个。


When I use gcov to measure test coverage of C++ code it reports branches in destructors.

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

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

When I run gcov with branch probabilities enabled (-b) I get the following output.

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

The part that bothers me is the "Taken at least once:50.00% of 2".

The generated .gcov file gives more detail.

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

Notice the line "branch 0 taken 0% (fallthrough)".

What causes this branch and what do I need to do in the code to get a 100% here?

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

解决方案

In a typical implementation the destructor usually has two branches: one for non-dynamic object destruction, another for dynamic object destruction. The selection of a specific branch is performed through a hidden boolean parameter passed to the destructor by the caller. It is usually passed through a register as either 0 or 1.

I would guess that, since in your case the destruction is for a non-dynamic object, the dynamic branch is not taken. Try adding a new-ed and then delete-ed object of class Foo and the second branch should become taken as well.

The reason this branching is necessary is rooted in the specification of C++ language. When some class defines its own operator delete, the selection of a specific operator delete to call is done as if it was looked up from inside the class destructor. The end result of that is that for classes with virtual destructor operator delete behaves as if it were a virtual function (despite formally being a static member of the class).

Many compilers implement this behavior literally: the proper operator delete is called directly from inside the destructor implementation. Of course, operator delete should only be called when destroying dynamically allocated objects (not for local or static objects). To achieve this, the call to operator delete is placed into a branch controlled by the hidden parameter mentioned above.

In your example things look pretty trivial. I'd expect the optimizer to remove all unnecessary branching. However, it appears that somehow it managed to survive optimization.


Here's a little bit of additional research. Consider this 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;
} 

This is how the code for the destructor of A will look like when compiler with GCC 4.3.4 under default optimization settings

__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

(The destructor of B is a bit more complicated, which is why I use A here as an example. But as far as the branching in question is concerned, destructor of B does it in the same way).

However, right after this destructor the generated code contains another version of the destructor for the very same class A, which looks exactly the same, except the movl $0, %eax instruction is replaced with movl $1, %eax instruction.

__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

Note the code blocks I labeled with arrows. This is exactly what I was talking about. Register al serves as that hidden parameter. This "pseudo-branch" is supposed to either invoke or skip the call to operator delete in accordance with the value of al. However, in the first version of the destructor this parameter is hardcoded into the body as always 0, while in the second one it is hardcoded as always 1.

Class B also has two versions of the destructor generated for it. So we end up with 4 distinctive destructors in the compiled program: two destructors for each class.

I can guess that at the beginning the compiler internally thought in terms of a single "parameterized" destructor (which works exactly as I described above the break). And then it decided to split the parameterized destructor into two independent non-parameterized versions: one for the hardcoded parameter value of 0 (non-dynamic destructor) and another for the hardcoded parameter value of 1 (dynamic destructor). In non-optimized mode it does that literally, by assigning the actual parameter value inside the body of the function and leaving all the branching totally intact. This is acceptable in non-optimized code, I guess. And that's exactly what you are dealing with.

In other words, the answer to your question is: It is impossible to make the compiler to take all the branches in this case. There's no way to achieve 100% coverage. Some of these branches are "dead". It is just that the approach to generating non-optimized code is rather "lazy" and "loose" in this version of GCC.

There might be a way to prevent the split in non-optimized mode, I think. I just haven't found it yet. Or, quite possibly, it can't be done. Older versions of GCC used true parameterized destructors. Maybe in this version of GCC they decided to switch to two-destructor approach and while doing it they "reused" the existing code-generator in such a quick-and-dirty way, expecting the optimizer to clean out the useless branches.

When you are compiling with optimization enabled GCC will not allow itself such luxuries as useless branching in the final code. You should probably try to analyze optimized code. Non-optimized GCC-generated code has lots of meaningless inaccessible branches like this one.

这篇关于gcov报告的析构函数中的分支是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆