web-dev-qa-db-ja.com

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'

気になるのは「1回以上撮った: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
40
Eddy Pronk

通常の実装では、デストラクタには通常2つのブランチがあります。1つは非動的オブジェクト破壊用、もう1つは動的オブジェクト破壊用です。特定のブランチの選択は、呼び出し元によってデストラクタに渡される非表示のブールパラメータを介して実行されます。通常、0または1としてレジスタを通過します。

あなたの場合、破壊は非動的オブジェクトに対するものであるため、動的分岐は行われないと思います。クラスnewdelete- ed、次にFoo- edオブジェクトを追加してみてください。そうすれば、2番目のブランチも取得されるはずです。

この分岐が必要な理由は、C++言語の仕様に基づいています。一部のクラスが独自のoperator deleteを定義する場合、呼び出す特定のoperator deleteの選択は、クラスデストラクタ内から検索されたかのように行われます。その結果、仮想デストラクタoperator deleteを持つクラスは、virtual関数であるかのように動作します(正式にはstaticクラスのメンバー)。

多くのコンパイラはこの動作を実装します文字通り:適切な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;
} 

これは、デフォルトの最適化設定で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を使用します。ただし、問題の分岐に関する限り、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は、その隠しパラメータとして機能します。この「疑似ブランチ」は、alの値に従って、operator deleteへの呼び出しを呼び出すかスキップすることになっています。ただし、デストラクタの最初のバージョンでは、このパラメータは常に0として本体にハードコードされていますが、2番目のバージョンでは常に1としてハードコードされています。

クラスBには、そのために生成された2つのバージョンのデストラクタもあります。したがって、コンパイルされたプログラムには4つの特徴的なデストラクタがあります。クラスごとに2つのデストラクタです。

当初、コンパイラーは、単一の「パラメーター化された」デストラクタ(ブレークの上で説明したとおりに機能する)の観点から内部的に考えていたと推測できます。次に、パラメータ化されたデストラクタを2つの独立したパラメータ化されていないバージョンに分割することを決定しました。1つはハードコードされたパラメータ値0(非動的デストラクタ)用で、もう1つはハードコードされたパラメータ値1(動的デストラクタ)。非最適化モードでは、関数の本体内に実際のパラメーター値を割り当て、すべての分岐を完全にそのままにして、文字通りそれを行います。これは、最適化されていないコードでは許容できると思います。そしてそれはまさにあなたが扱っているものです。

言い換えれば、あなたの質問に対する答えは次のとおりです。この場合、コンパイラにすべてのブランチを取得させることは不可能です。100%のカバレッジを達成する方法はありません。これらのブランチの一部は「デッド」です。このバージョンのGCCでは、最適化されていないコードを生成するアプローチがかなり「怠惰」で「緩い」というだけです。

最適化されていないモードでの分割を防ぐ方法があるかもしれないと思います。まだ見つけていません。または、おそらく、それはできません。古いバージョンのGCCは、真のパラメーター化されたデストラクタを使用していました。たぶん、このバージョンのGCCでは、2つのデストラクタのアプローチに切り替えることを決定し、その間、オプティマイザが役に立たないブランチをクリーンアップすることを期待して、既存のコードジェネレータをそのような迅速で汚い方法で「再利用」しました。

最適化を有効にしてコンパイルしている場合、GCCは、最終的なコードでの無駄な分岐などの贅沢を許可しません。おそらく、最適化されたコードの分析を試みる必要があります。最適化されていないGCCで生成されたコードには、このような意味のないアクセスできないブランチがたくさんあります。

56
AnT

デストラクタで、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>
[...]
7
Adam Mitz

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 ...」を使用して、説明されているようにカバレッジを生成します ここ

0
Andy