C ++で例外が(舞台裏で)機能する方法


109

例外は遅いと人々が言っ​​ているのを見続けていますが、証拠はありません。それで、それらがそうであるかどうかを尋ねるのではなく、例外が舞台裏でどのように機能するかを尋ねるので、それらをいつ使用するか、そしてそれらが遅いかどうかの決定をすることができます。

私の知っている限りでは、例外は、何度もリターンを行うのと同じですが、各リターンの後に、別のリターンを行う必要があるか、停止する必要があるかどうかもチェックします。戻るのをいつ停止するかをどのように確認しますか?例外のタイプとスタックの場所を保持する2番目のスタックがあると思います。その後、そこに到達するまで戻ります。また、この2番目のスタックがタッチされるのは、スローと各トライ/キャッチ時だけだと思います。AFAICTが戻りコードを使用して同様の動作を実装すると、同じ時間がかかります。しかし、これはすべて推測にすぎないので、実際に何が起こっているのかを知りたいのです。

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



回答:


105

推測する代わりに、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

_ZN11MyExceptionD1Evis 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:

驚き!通常のコードパスには、追加の命令はありません。コンパイラーは代わりに、関数の最後にあるテーブルを介して参照される追加のアウトオブライン修正コードブロックを生成しました(実際には実行可能ファイルの別のセクションに配置されています)。すべての作業は、これらのテーブル(_ZTI11MyExceptionis 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_関数の機能が必要な場合は、元の仕様を参照してください。


23
要約です。例外がスローされなければ費用はかかりません。例外がスローされると多少のコストがかかりますが、問題は「このコストは、エラーコードを使用してエラー処理コードに戻るまでテストするよりもコストが高いですか」です。
マーティンヨーク

5
エラーのコストは確かに大きいでしょう。例外コードはまだディスク上にある可能性があります!エラー処理コードが通常のコードから削除されているため、エラー以外の場合のキャッシュ動作が向上します。
MSalters 2008年

ARMなどの一部のプロセッサでは、「bl」[branch-and-link、別名「call」]命令を過ぎて「余分な」8バイトのアドレスに戻ると、次のアドレスに戻るのと同じコストがかかります。 「bl」。各「bl」の後に「着信例外」ハンドラーのアドレスが続くだけの効率は、テーブルベースのアプローチのそれとどのように比較され、コンパイラーがそのようなことをするかどうか疑問に思います。私が見ることができる最大の危険は、呼び出し規約の不一致が奇妙な動作を引き起こす可能性があることです。
スーパーキャット2012年

2
@supercat:この方法で例外処理コードでIキャッシュを汚染しています。結局のところ、例外処理コードとテーブルが通常のコードから遠く離れている傾向があるのには理由があります。
CesarB

1
@CesarB:各呼び出しに続く1つの命令ワード。特に「外部」コードのみを使用した例外処理の手法では、通常、コードが常に有効なフレームポインタを維持する必要があることを考えると、とんでもないように見えません(場合によっては、追加の命令が不要な場合もありますが、それ以外の場合は、 1)。
スーパーキャット

13

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

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

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

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

また、初心者のための1つの落とし穴があります。
ます。Exceptionオブジェクトは小さいと想定されていますが、中にはたくさんのものを入れる人もいます。次に、例外オブジェクトをコピーするコストがかかります。解決策は2つあります。

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

私の意見では、例外のある同じコードはより効率的であるか、少なくとも例外のないコードと同等である(ただし、関数エラーの結果をチェックするための追加のコードはすべてある)と思います。何も無料で入手していないことを思い出してください。コンパイラは、最初にエラーコードをチェックするために記述すべきコードを生成しています(通常、コンパイラは人間よりもはるかに効率的です)。


1
知覚された速度の遅さのためではなく、彼らがどのように実装されているのか、そしてあなたのコードに対して何をしているのかを知らないので、人々は例外の使用をためらうと思う。彼らが魔法のように見えるという事実は、金属に近いタイプの多くを怒らせます。
スピードプレーン2016年

@speedplane:たぶん。しかし、コンパイラの重要なポイントは、ハードウェアを理解する必要がないようにすることです(抽象化層を提供します)。現代のコンパイラーでは、現代のC ++コンパイラーのあらゆる側面を理解する人を1人見つけることができるかどうか疑問に思います。では、なぜ理解複雑な機能X.異なる理解の例外がある
マーティンニューヨーク

あなたは常にハードウェアが何をしているかについて何らかの考えを持っている必要があります、それは程度の問題です。(Javaまたはスクリプト言語を介して)C ++を使用している多くの人は、パフォーマンスのためにしばしばそうしています。彼らにとって、抽象化レイヤーは比較的透明でなければならないので、金属で何が起こっているのかをある程度理解することができます。
スピードプレーン2016年

@speedplane:次に、抽象層が設計によりはるかに薄い場所であるCを使用する必要があります。
マーティンヨーク

12

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

コードプロジェクトの詳細については、きちんと議論されています。C++コンパイラが例外処理を実装する方法

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

リターンコードを使用すると、一度に1つのレベルのスタックのみを巻き戻すことができますが、例外処理メカニズムは、中間スタックフレームで何も実行しない場合、1回の操作でスタックをさらに下にジャンプできます。


「例外のオーバーヘッドが発生するのは、各スタックフレーム(より正確にはスコープ)で破棄する必要があるオブジェクトを追跡するコードをコンパイラーが生成する必要があるためです。

いいえ。戻りアドレスとテーブルを含むスタックが与えられた場合、コンパイラはスタックにある関数を判別できます。それから、どのオブジェクトがスタック上にあったに違いありません。これは、例外がスローされた後に実行できます。少し高価ですが、例外が実際にスローされたときにのみ必要です。
MSalters 2008年

陽気なだけで、「各スタックフレームがその中のオブジェクトの数、型、名前を追跡し、関数がスタックを掘り、デバッグ中にどのスコープを継承するかを確認できるようになっているのではないか」と思いました。 、そしてある意味で、これはそのようなことを行いますが、手動で常にすべてのスコープの最初の変数としてテーブルを宣言する必要はありません。
ドミトリー


5

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



0

すべての良い答え。

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

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


私は共通して同意しますが、場合によっては例外によりコードが簡略化されることがあります。コンストラクターでのエラー処理について考えてください
...-
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.