126

例外は遅いと言う人をよく見かけますが、その証拠は見当たりません。したがって、そうであるかどうかを尋ねる代わりに、例外が舞台裏でどのように機能するかを尋ねます。これにより、例外をいつ使用するか、例外が遅いかどうかを判断できます。

私が知っていることから、例外は、リターンの束を何度も実行するのと同じですが、リターンのたびに、別のリターンを実行する必要があるか、停止する必要があるかどうかもチェックします。復帰を停止するタイミングをどのように確認しますか? 例外のタイプとスタックの場所を保持する2番目のスタックがあると思いますが、そこに到達するまで戻ります。また、この 2 番目のスタックに触れるのは、スローと各 try/catch のみであると推測しています。リターンコードを使用して同様の動作を実装する AFAICT には、同じ時間がかかります。しかし、これはあくまでも推測なので、実際にどうなるか知りたいです。

例外は実際にどのように機能しますか?

4

7 に答える 7

115

推測する代わりに、生成されたコードを、小さな 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

_ZN11MyExceptionD1EvであるMyException::~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:

サプライズ!通常のコード パスには、余分な命令はまったくありません。代わりに、コンパイラは余分な行外の修正コード ブロックを生成し、関数の最後にあるテーブルを介して参照されます (実際には、実行可能ファイルの別のセクションに配置されます)。_ZTI11MyExceptionすべての作業は、これらのテーブル ( is )に基づいて、標準ライブラリによって舞台裏で行われtypeinfo for MyExceptionます。

わかりました、それは私にとって実際には驚きではありませんでした.私はすでにこのコンパイラがそれをどのように行ったかを知っていました. アセンブリの出力を続けます。

    .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_関数が何をするかについては、元の仕様を参照してください。

于 2008-11-21T03:16:53.590 に答える
13

遅いという例外は、昔は本当でした。
最近のほとんどのコンパイラでは、これは当てはまりません。

注: 例外があるからといって、エラー コードも使用しないわけではありません。エラーをローカルで処理できる場合は、エラー コードを使用します。エラーが訂正のためにより多くのコンテキストを必要とする場合は、例外を使用してください:私はここでそれをより雄弁に書きました: 例外処理ポリシーを導く原則は何ですか?

例外が使用されていない場合、例外処理コードのコストは実質的にゼロです。

例外がスローされると、何らかの作業が行われます。
ただし、これをエラー コードを返し、エラーを処理できる場所までさかのぼってチェックするコストと比較する必要があります。作成と保守の両方に時間がかかります。

また、初心者向けの注意点が 1 つあり
ます。例外オブジェクトは小さいはずですが、中にはたくさんのものを入れている人もいます。次に、例外オブジェクトをコピーするコストがかかります。解決策は 2 つあります。

  • 例外に余分なものを入れないでください。
  • const 参照でキャッチします。

私の意見では、例外を含む同じコードは、例外を含まないコードよりも効率的であるか、少なくとも同等であると確信しています (ただし、関数のエラー結果をチェックするための余分なコードがすべて含まれています)。無料で何も得られないことを覚えておいてください。コンパイラは、エラーコードをチェックするために最初に記述すべきコードを生成しています (通常、コンパイラは人間よりもはるかに効率的です)。

于 2008-11-21T03:21:44.890 に答える
12

例外を実装する方法はいくつかありますが、通常は OS の基本的なサポートに依存します。Windows では、これは構造化された例外処理メカニズムです。

Code Project の詳細については、まともな議論があります: How a C++ compiler implements exception handling

例外のオーバーヘッドが発生するのは、例外がそのスコープ外に伝播した場合に、各スタック フレーム (より正確にはスコープ) でどのオブジェクトを破棄する必要があるかを追跡するコードをコンパイラが生成する必要があるためです。関数のスタックに、デストラクタの呼び出しが必要なローカル変数がない場合、例外処理に関してパフォーマンスが低下することはありません。

戻りコードを使用すると、一度に 1 レベルのスタックしかアンワインドできませんが、例外処理メカニズムは、中間スタック フレームで何もする必要がない場合、1 回の操作でスタックをさらに下にジャンプできます。

于 2008-11-21T02:13:07.540 に答える
6

Matt Pietrek は、 Win32 Structured Exception Handlingに関する優れた記事を書きました。この記事は 1997 年に最初に書かれましたが、現在でも適用されます (ただし、もちろん Windows にのみ適用されます)。

于 2008-11-21T02:17:27.640 に答える
5

この記事では、この問題を調べて、例外がスローされない場合のコストはかなり低くなりますが、実際には例外には実行時のコストがかかることを基本的に発見しました。良い記事、お勧めします。

于 2008-11-21T02:23:26.473 に答える
2

私の友人は、数年前に Visual C++ が例外を処理する方法について少し書きました。

http://www.xyzw.de/c160.html

于 2008-11-21T02:18:26.170 に答える
0

すべての良い答え。

また、コードが例外をスローするのではなく、メソッドの上部でゲートとして「if チェック」を行うコードをデバッグする方が、はるかに簡単であると考えてください。

私のモットーは、機能するコードを簡単に書くことです。最も重要なことは、次にそれを見る人のためにコードを書くことです。場合によっては、9 か月後のあなたであり、自分の名前をののしりたくない場合もあります。

于 2008-11-21T02:23:05.773 に答える