gcov报告的析构函数中的分支是什么?
当我使用gcov来度量C ++代码的testing覆盖率时,它会在析构函数中报告分支。
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'
困扰我的部分是“至less一次: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: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
在一个典型的实现中,析构函数通常有两个分支:一个用于非dynamic对象销毁,另一个用于dynamic对象销毁。 调用者通过传递给析构函数的隐藏布尔参数来执行特定分支的select。 它通常通过寄存器0或1。
我猜想,因为在你的情况下,销毁是针对非dynamic对象的,dynamic分支不会被采用。 尝试添加一个new
,然后delete
Foo
类的对象,第二个分支也应该被采用。
这种分支是必要的原因是植根于C ++语言的规范。 当某个类定义了它自己的operator delete
,select一个特定的operator delete
来调用,就像从类析构函数中查找它一样。 最终的结果是,具有虚拟析构函数operator delete
performance得像是一个虚函数(尽pipe正式成为类的静态成员)。
许多编译器实际上实现了这种行为:正确的operator delete
直接从析构函数实现中调用。 当然, operator delete
只能在销毁dynamic分配的对象(不适用于本地或静态对象)时调用。 为了达到这个目的,对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; }
这就是在默认优化设置下使用GCC 4.3.4进行编译时, A
析构函数的代码的样子
__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
这里的A
为例,但就分支问题而言, B
析构函数也是这样做的)。
然而,在这个析构函数之后,生成的代码包含了与A
类 完全相同 的析构函数的另一个版本 ,除了movl $0, %eax
指令被movl $1, %eax
指令replace之外,看起来完全一样 。
__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个独特的析构函数:每个类都有两个析构函数。
我可以猜测,编译器在开始时就内部考虑了单个“参数化”的析构函数(其工作原理与我上面介绍的那样)。 然后决定将参数化析构函数拆分成两个独立的非参数化版本:一个用于硬编码参数值0
(非dynamic析构函数),另一个用于硬编码参数值1
(dynamic析构函数)。 在非优化模式下,通过在函数体内分配实际的参数值,并使所有分支完全保持原样,从字面上做到这一点。 我猜,这在非优化代码中是可以接受的。 而这正是你正在处理的。
换句话说,你的问题的答案是: 在这种情况下,编译器不可能取所有的分支。 没有办法达到100%的覆盖率。 其中一些分支是“死亡”的。 在这个版本的GCC中,生成非优化代码的方法相当“懒惰”和“松散”。
我想可能有办法阻止非优化模式的分裂。 我只是还没有find它。 或者很可能,这是不可能的。 老版本的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> [...]