1. ホーム
  2. c++

[解決済み] c++で例外はどのように動作するのか?

2022-07-28 05:45:55

質問

例外処理は遅いと言われ続けていますが、その証拠を見たことがありません。そこで、「遅いかどうか」ではなく、「例外は裏でどう動いているのか」を聞いて、「いつ使うのか」「遅いかどうか」を判断できるようにしたいと思っています。

私が知っている限り、例外はreturnを何度も行うのと同じです。ただし、returnを行うたびに、次のreturnを行う必要があるか、停止する必要があるかをチェックします。リターンを停止するタイミングはどのように確認するのでしょうか。私は、例外の型とスタックの位置を保持する第2のスタックがあり、そこに到達するまでリターンを実行するのだと思います。この2つ目のスタックに触れるのはthrowとtry/catchのときだけだと思います。推測ですが、リターンコードで同様の動作を実装しても、同じ時間がかかると思います。しかし、これはすべて推測に過ぎないので、実際に何が起こるかを知りたいのです。

例外は実際にどのように動作するのでしょうか?

どのように解決するのですか?

推測する代わりに、C++ コードの小片とやや古い Linux インストールで、生成されたコードを実際に見てみることにしました。

class MyException
{
public:
    MyException() { }
    ~MyException() { }
};

void my_throwing_function(bool throwit)
{
    if (throwit)
        throw MyException();
}

void another_function();
void log(unsigned count);

void my_catching_function()
{
    log(0);
    try
    {
        log(1);
        another_function();
        log(2);
    }
    catch (const MyException& e)
    {
        log(3);
    }
    log(4);
}

でコンパイルしました。 g++ -m32 -W -Wall -O3 -save-temps -c でコンパイルし、生成されたアセンブリファイルを見てみました。

    .file   "foo.cpp"
    .section    .text._ZN11MyExceptionD1Ev,"axG",@progbits,_ZN11MyExceptionD1Ev,comdat
    .align 2
    .p2align 4,,15
    .weak   _ZN11MyExceptionD1Ev
    .type   _ZN11MyExceptionD1Ev, @function
_ZN11MyExceptionD1Ev:
.LFB7:
    pushl   %ebp
.LCFI0:
    movl    %esp, %ebp
.LCFI1:
    popl    %ebp
    ret
.LFE7:
    .size   _ZN11MyExceptionD1Ev, .-_ZN11MyExceptionD1Ev

_ZN11MyExceptionD1EvMyException::~MyException() であるため、コンパイラはデストラクタの非インラインコピーが必要であると判断しました。

.globl __gxx_personality_v0
.globl _Unwind_Resume
    .text
    .align 2
    .p2align 4,,15
.globl _Z20my_catching_functionv
    .type   _Z20my_catching_functionv, @function
_Z20my_catching_functionv:
.LFB9:
    pushl   %ebp
.LCFI2:
    movl    %esp, %ebp
.LCFI3:
    pushl   %ebx
.LCFI4:
    subl    $20, %esp
.LCFI5:
    movl    $0, (%esp)
.LEHB0:
    call    _Z3logj
.LEHE0:
    movl    $1, (%esp)
.LEHB1:
    call    _Z3logj
    call    _Z16another_functionv
    movl    $2, (%esp)
    call    _Z3logj
.LEHE1:
.L5:
    movl    $4, (%esp)
.LEHB2:
    call    _Z3logj
    addl    $20, %esp
    popl    %ebx
    popl    %ebp
    ret
.L12:
    subl    $1, %edx
    movl    %eax, %ebx
    je  .L16
.L14:
    movl    %ebx, (%esp)
    call    _Unwind_Resume
.LEHE2:
.L16:
.L6:
    movl    %eax, (%esp)
    call    __cxa_begin_catch
    movl    $3, (%esp)
.LEHB3:
    call    _Z3logj
.LEHE3:
    call    __cxa_end_catch
    .p2align 4,,3
    jmp .L5
.L11:
.L8:
    movl    %eax, %ebx
    .p2align 4,,6
    call    __cxa_end_catch
    .p2align 4,,6
    jmp .L14
.LFE9:
    .size   _Z20my_catching_functionv, .-_Z20my_catching_functionv
    .section    .gcc_except_table,"a",@progbits
    .align 4
.LLSDA9:
    .byte   0xff
    .byte   0x0
    .uleb128 .LLSDATT9-.LLSDATTD9
.LLSDATTD9:
    .byte   0x1
    .uleb128 .LLSDACSE9-.LLSDACSB9
.LLSDACSB9:
    .uleb128 .LEHB0-.LFB9
    .uleb128 .LEHE0-.LEHB0
    .uleb128 0x0
    .uleb128 0x0
    .uleb128 .LEHB1-.LFB9
    .uleb128 .LEHE1-.LEHB1
    .uleb128 .L12-.LFB9
    .uleb128 0x1
    .uleb128 .LEHB2-.LFB9
    .uleb128 .LEHE2-.LEHB2
    .uleb128 0x0
    .uleb128 0x0
    .uleb128 .LEHB3-.LFB9
    .uleb128 .LEHE3-.LEHB3
    .uleb128 .L11-.LFB9
    .uleb128 0x0
.LLSDACSE9:
    .byte   0x1
    .byte   0x0
    .align 4
    .long   _ZTI11MyException
.LLSDATT9:

驚きです。通常のコードパスには余分な命令は全くありません。代わりにコンパイラは、関数の末尾にあるテーブル (実際には実行ファイルの別のセクションに置かれます) を介して参照される、行外の修正コード ブロックを余分に生成しました。すべての作業は標準ライブラリが裏で行い、これらのテーブル ( _ZTI11MyExceptiontypeinfo for MyException ).

OK、実はこれは私にとって驚きではなく、このコンパイラがどうやるかはすでに知っていたのです。アセンブリの出力の続きです。

    .text
    .align 2
    .p2align 4,,15
.globl _Z20my_throwing_functionb
    .type   _Z20my_throwing_functionb, @function
_Z20my_throwing_functionb:
.LFB8:
    pushl   %ebp
.LCFI6:
    movl    %esp, %ebp
.LCFI7:
    subl    $24, %esp
.LCFI8:
    cmpb    $0, 8(%ebp)
    jne .L21
    leave
    ret
.L21:
    movl    $1, (%esp)
    call    __cxa_allocate_exception
    movl    $_ZN11MyExceptionD1Ev, 8(%esp)
    movl    $_ZTI11MyException, 4(%esp)
    movl    %eax, (%esp)
    call    __cxa_throw
.LFE8:
    .size   _Z20my_throwing_functionb, .-_Z20my_throwing_functionb

ここに、例外を投げるためのコードがあります。例外が投げられるかもしれないという理由だけで余分なオーバーヘッドはありませんでしたが、実際に例外を投げたりキャッチしたりする際には明らかに多くのオーバーヘッドがあります。そのほとんどは __cxa_throw の中に隠されており、これは必ず

  • その例外のハンドラを見つけるまで、例外テーブルの助けを借りてスタックを歩きます。
  • そのハンドラに到達するまで、スタックを巻き戻します。
  • 実際にハンドラを呼び出します。

単純に値を返す場合のコストと比較すると、例外は例外的な戻り値にのみ使用されるべき理由がわかるでしょう。

最後に、アセンブリファイルの残りの部分です。

    .weak   _ZTI11MyException
    .section    .rodata._ZTI11MyException,"aG",@progbits,_ZTI11MyException,comdat
    .align 4
    .type   _ZTI11MyException, @object
    .size   _ZTI11MyException, 8
_ZTI11MyException:
    .long   _ZTVN10__cxxabiv117__class_type_infoE+8
    .long   _ZTS11MyException
    .weak   _ZTS11MyException
    .section    .rodata._ZTS11MyException,"aG",@progbits,_ZTS11MyException,comdat
    .type   _ZTS11MyException, @object
    .size   _ZTS11MyException, 14
_ZTS11MyException:
    .string "11MyException"

typeinfoのデータです。

    .section    .eh_frame,"a",@progbits
.Lframe1:
    .long   .LECIE1-.LSCIE1
.LSCIE1:
    .long   0x0
    .byte   0x1
    .string "zPL"
    .uleb128 0x1
    .sleb128 -4
    .byte   0x8
    .uleb128 0x6
    .byte   0x0
    .long   __gxx_personality_v0
    .byte   0x0
    .byte   0xc
    .uleb128 0x4
    .uleb128 0x4
    .byte   0x88
    .uleb128 0x1
    .align 4
.LECIE1:
.LSFDE3:
    .long   .LEFDE3-.LASFDE3
.LASFDE3:
    .long   .LASFDE3-.Lframe1
    .long   .LFB9
    .long   .LFE9-.LFB9
    .uleb128 0x4
    .long   .LLSDA9
    .byte   0x4
    .long   .LCFI2-.LFB9
    .byte   0xe
    .uleb128 0x8
    .byte   0x85
    .uleb128 0x2
    .byte   0x4
    .long   .LCFI3-.LCFI2
    .byte   0xd
    .uleb128 0x5
    .byte   0x4
    .long   .LCFI5-.LCFI3
    .byte   0x83
    .uleb128 0x3
    .align 4
.LEFDE3:
.LSFDE5:
    .long   .LEFDE5-.LASFDE5
.LASFDE5:
    .long   .LASFDE5-.Lframe1
    .long   .LFB8
    .long   .LFE8-.LFB8
    .uleb128 0x4
    .long   0x0
    .byte   0x4
    .long   .LCFI6-.LFB8
    .byte   0xe
    .uleb128 0x8
    .byte   0x85
    .uleb128 0x2
    .byte   0x4
    .long   .LCFI7-.LCFI6
    .byte   0xd
    .uleb128 0x5
    .align 4
.LEFDE5:
    .ident  "GCC: (GNU) 4.1.2 (Ubuntu 4.1.2-0ubuntu4)"
    .section    .note.GNU-stack,"",@progbits

さらに多くの例外処理テーブルと、分類された追加情報。

つまり、少なくとも Linux 上の GCC での結論は、例外が投げられるかどうかにかかわらず、コストは (ハンドラとテーブルのための) 余分なスペースであり、さらに例外が投げられたときにテーブルを解析しハンドラを実行する余分なコストとなります。もし、エラーコードの代わりに例外を使い、エラーが稀であれば、それは より速く というのも、エラーをテストするためのオーバーヘッドがもうないからです。

より詳細な情報が必要な場合、特に、すべての __cxa_ 関数が何をするのか、といった詳細な情報は、それらの元となった仕様書を参照してください。