What Is the Branch in the Destructor Reported by Gcov

What is the branch in the destructor reported by gcov?

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.

Understanding branches in gcov files

Gcov works by instrumenting (while compiling) every basic block of machine commands (you can think about assembler). Basic block means a linear section of code, which have no branches inside it and no lables inside it. So, If and only if you start running a basic block, you will reach end of basic block. Basic blocks are organized in CFG (Control flow graph, think about it as directed graph), which shows relations between basicblocks (edge from V1 to V2 is V1 calls V2; and V2 is called by V1). So, profile-arcs mode of compiler and gcov want to get execution count for every line and do this via counting basic block executions. Some of edges in CFG are instrumented and some are not, because there are algebraic relations between basic blocks in graph.

Your ObjC construction (for..in) is lowered (converted in early compilation) to several basic blocks. So, gcov sees 4 branches, because it sees only lowered BBs. It knows nothing about this lowering, but it knows which line corresponds to every assembler instruction (this is debug info). So, branches are edges of CFG.

If you want to see basic blocks, you should do an assembler dump of compiled program or disassemble a binary or dump CFG from compiler. You can do this both for profile-arcs and non-profile-arcs modes and compare them.

profile-arcs mode will have a lot calls and increments of something like "__llvm_gcov_ctr" or "__llvm_gcda_edge" - it is an actual instrumentation of basic blocks.

How many branches are in a (a && b) statement? According to gcov: 4

Apparently gcov is considering a&&b to be the following:

if(a) { // branch 1
if(b) { // branch 2
1;
} else { // branch 3
0;
}
} else { // branch 4
0;
}

Though I'm fairly certain the actual CPU instructions will translate to a single branch.

Does Branch coverage implies Condition Coverage?

If you're talking specifically about the gcov tools, the branch coverage is really counting the points at which the end of each basic block is reached. See Understanding branches in gcov files.

Condition coverage is something different. See Is it possible to check condition coverage with gcov? (The answer is no, it's not, at least not with just gcov.) As Marc Gilsse's comment notes, you can use gcov -a to count executions through each basic block, and use that as a reasonable approximation—but it's not the same thing.

C++'s exceptions make dealing with branch coverage messy; see LCOV/GCOV branch coverage with C++ producing branches all over the place. This means you won't necessarily be able to cover all branches in the first place.

See also, e.g., Understanding blocks in gcov files.

Gcovr branch coverage for simple case

This is an issue with gcov. If you look at the underlying gcov output [which the example driver is so courteous to leave for us as build-run/^#main.cpp.gcov], you see:

[…snip…]
3: 21: return 0;
function _Z41__static_initialization_and_destruction_0ii called 3 returned 100% blocks executed 100%
6: 22:}
branch 0 taken 3 (fallthrough)
branch 1 taken 0
branch 2 taken 3 (fallthrough)
branch 3 taken 0
function _GLOBAL__I_main called 3 returned 100% blocks executed 100%
3: 23:/*EOF*/
call 0 returned 3

I think what is being reported is branch coverage for the destructors of static members of objects in the iostream library. … while we try and filter out most of the gcov weirdness through gcovr, this is one of the cases that we cannot reliably ignore.

Bill Hart
John Siirola

P.S. I encourage you to submit gcovr tickets on the gcovr Trac page: https://software.sandia.gov/trac/fast/newticket

How to show True/False Condition on Branch Coverage on Gcovr

The gcovr XML output uses the Cobertura XML format that is understood by a variety of other tools. This means gcovr is limited to that XML schema, and cannot include extra information.

Gcovr's HTML reports (--html-details) display branch coverage. To see which branches are uncovered, it is often easiest to see which statements in the conditional branches are uncovered. However, the branch coverage column also displays small icons that indicate which branches were taken. There is one indicator per branch / two per condition. A green ✔ indicates a covered branch, a red ✘ an uncovered branch.

screenshot of branch indicators in a gcovr HTML report

In the above example, the first branch is uncovered. Whether the first branch corresponds to a true or false condition is not defined. If in doubt, rewrite your code to only use statement-level conditionals (no … ? … : …, &&, || operators) so that all branches are separate statements. Note that in C++, exception handling can introduce additional branches that may be difficult/impossible to cover.

Tip: the --branch option is unnecessary to get branch coverage. It only controls whether the gcovr text report shows branch or line coverage.



Related Topics



Leave a reply



Submit