g++でプログラムを組んでいて、意外に実行速度の邪魔になるのが例外処理のスタック巻き戻しコード。 スタック巻き戻しコードとは、ある関数が他の関数を呼び出し、呼び出し先で例外が発生した場合に、呼び出し元が実行すべきデストラクタを津々浦々、粛々と呼び出すためのコード。 通常、関数に入ったところでデストラクタ呼び出し部分を登録して、関数を出るところで登録を抹消する。 例えばこんなコード。
class C { public: ~C(); }; void bar(); void foo() { C c; bar(); }
g++ -O2 -S とすると、こんな長いコードが生成される。ちなみにCygwin付属のgcc 3.4.4にてコンパイル。
__Z3foov: pushl %ebp movl %esp, %ebp leal -24(%ebp), %eax subl $120, %esp movl %eax, -60(%ebp) leal -92(%ebp), %eax movl %ebx, -12(%ebp) movl %esi, -8(%ebp) movl %edi, -4(%ebp) movl $___gxx_personality_sj0, -68(%ebp) movl $LLSDA2, -64(%ebp) movl $L6, -56(%ebp) movl %esp, -52(%ebp) movl %eax, (%esp) call __Unwind_SjLj_Register movl $1, -88(%ebp) call __Z3barv L1: movl $-1, -88(%ebp) leal -40(%ebp), %eax movl %eax, (%esp) call __ZN1CD1Ev leal -92(%ebp), %eax movl %eax, (%esp) call __Unwind_SjLj_Unregister movl -12(%ebp), %ebx movl -8(%ebp), %esi movl -4(%ebp), %edi movl %ebp, %esp popl %ebp ret .p2align 4,,7 L6: L2: L4: addl $24, %ebp movl $0, -88(%ebp) movl -84(%ebp), %eax movl %eax, -96(%ebp) leal -40(%ebp), %eax movl %eax, (%esp) call __ZN1CD1Ev movl $-1, -88(%ebp) movl -96(%ebp), %eax movl %eax, (%esp) call __Unwind_SjLj_Resume
ある関数で発生するかもしれない例外は、例外指定として書いておくことができる。 これを使ってbarが例外を出さないと明示してやれば、
class C { public: ~C(); }; void bar() throw(); void foo() { C c; bar(); }
こんなに短いコードになる。
__Z3foov: L2: pushl %ebp movl %esp, %ebp subl $40, %esp call __Z3barv leal -24(%ebp), %eax movl %eax, (%esp) call __ZN1CD1Ev leave ret
スタックの巻き戻し処理は、デストラクタを呼び出すためのものだから、barが例外を出すかもしれない場合でも、デストラクタさえなければ、
class C { public: C(); // コンストラクタはあるが、デストラクタはない }; void bar(); void foo() { C c; bar(); }
やっぱり短く済む。
__Z3foov: pushl %ebp movl %esp, %ebp leal -1(%ebp), %eax subl $8, %esp movl %eax, (%esp) call __ZN1CC1Ev call __Z3barv leave ret
でも、基底クラスにデストラクタがあれば、それを呼ばないといけないから、
class C { public: ~C(); }; class D : public C { // デストラクタがないように見えるけど、 // C::~C() は呼ばなければならない。 }; void bar(); void foo() { C c; bar(); }
長くなっちゃう。
__Z3foov: pushl %ebp movl %esp, %ebp leal -24(%ebp), %eax subl $120, %esp movl %eax, -60(%ebp) leal -92(%ebp), %eax movl %ebx, -12(%ebp) movl %esi, -8(%ebp) movl %edi, -4(%ebp) movl $___gxx_personality_sj0, -68(%ebp) movl $LLSDA2, -64(%ebp) movl $L6, -56(%ebp) movl %esp, -52(%ebp) movl %eax, (%esp) call __Unwind_SjLj_Register movl $1, -88(%ebp) call __Z3barv L1: movl $-1, -88(%ebp) leal -40(%ebp), %eax movl %eax, (%esp) call __ZN1CD1Ev leal -92(%ebp), %eax movl %eax, (%esp) call __Unwind_SjLj_Unregister movl -12(%ebp), %ebx movl -8(%ebp), %esi movl -4(%ebp), %edi movl %ebp, %esp popl %ebp ret .p2align 4,,7 L6: L2: L4: addl $24, %ebp movl $0, -88(%ebp) movl -84(%ebp), %eax movl %eax, -96(%ebp) leal -40(%ebp), %eax movl %eax, (%esp) call __ZN1CD1Ev movl $-1, -88(%ebp) movl -96(%ebp), %eax movl %eax, (%esp) call __Unwind_SjLj_Resumeデストラクタはコンストラクタが呼び出されていなければ呼び出す必要はない(というか、呼び出してはいけない)ので、barを呼び出す前にコンストラクタが呼び出されていなければ、
class C { public: ~C(); }; void bar(); void foo() { bar(); // 先にbarを呼んで、 C c; // それからCを作る。 }
余計なコードは生成されない。
__Z3foov: L2: pushl %ebp movl %esp, %ebp subl $40, %esp call __Z3barv leal -24(%ebp), %eax movl %eax, (%esp) call __ZN1CD1Ev leave ret
あるいは、デストラクタを呼び終わった後にbarを呼べば、
class C { public: ~C(); }; void bar(); void foo() { { C c; } // ここでデストラクタは実行済み bar(); }
やっぱり短い。
__Z3foov: L2: pushl %ebp movl %esp, %ebp leal -24(%ebp), %eax subl $40, %esp movl %eax, (%esp) call __ZN1CD1Ev call __Z3barv leave ret
動的に生成するオブジェクトは、関数を抜けるときにデストラクタが呼ばれるわけではないので、
class C { public: ~C(); }; void bar(); C *foo() { C *pc = new C; bar(); return pc; }
何をやったって短い。
__Z3foov: pushl %ebp movl %esp, %ebp pushl %ebx subl $4, %esp movl $1, (%esp) call __Znwj movl %eax, %ebx call __Z3barv popl %edx movl %ebx, %eax popl %ebx popl %ebp ret
動的に生成したオブジェクトのデストラクタはdeleteの時に呼び出される。 例外でdeleteが飛ばされた場合は、どこか他でdeleteしなければならない約束になっているので、
class C { public: ~C(); }; void bar(); void foo() { C *pc = new C; bar(); delete pc; }
__Z3foov: pushl %ebp movl %esp, %ebp pushl %ebx subl $4, %esp movl $1, (%esp) call __Znwj movl %eax, %ebx call __Z3barv testl %ebx, %ebx je L1 movl %ebx, (%esp) call __ZN1CD1Ev movl %ebx, (%esp) call __ZdlPv L1: popl %eax popl %ebx popl %ebp ret
やっぱり余計なコードは生成されない。 ちなみに、「ポインタがNULLだったらdeleteは飛ばす」というコードも入っている。
なんかややこしいようだけど、ルールは意外に単純。
g++の場合、デストラクタのある自動変数の構築が終わってから破壊されるまでの間に、例外が起きる可能性があればスタック巻き戻しコードが生成される。
メンバ変数のコンストラクタ・デストラクタは、親オブジェクトのコンストラクタ・デストラクタの中から呼び出される。 「デストラクタのあるメンバ」を持っているオブジェクトは、明示的にデストラクタを宣言していなくてもデストラクタがあることになる。 これは基底クラスがデストラクタを持っている場合と同じ。
「関数の外で既に生成されているオブジェクト」のメンバを使う場合、元々の親オブジェクトが自動変数ならこの関数内で破壊されることはない。 引数でオブジェクトをもらう場合や、メンバ関数の場合によくあるパターンで、この場合、メンバはなんぼ使ってもスタック巻き戻しコードの有無に影響しない。 親オブジェクトが動的に生成された場合は、そもそもスタック巻き戻しには関係ない(deleteしない限りデストラクタは呼ばれないから)。 親オブジェクトがどこで生成されたか?が重要。
「例外が起きる可能性がある」というのは例外を投げる可能性のある関数を呼び出す、というのとほぼ同じ意味で、この「関数」にはコンストラクタやデストラクタも含まれる。だから、デストラクタのあるオブジェクトを作ってから、例外を投げる可能性のあるコンストラクタが実行されると、
class C { // この人は例外を投げない。 public: ~C() throw(); }; class D { public: D(); // 例外を投げるかもしれない。 }; void foo() { C c; D d; // コンストラクタが例外を投げるかも。 }
長くなる。
__Z3foov: pushl %ebp movl %esp, %ebp leal -24(%ebp), %eax subl $120, %esp movl %eax, -60(%ebp) leal -92(%ebp), %eax movl %eax, (%esp) movl %ebx, -12(%ebp) movl %esi, -8(%ebp) movl %edi, -4(%ebp) movl $___gxx_personality_sj0, -68(%ebp) movl $LLSDA2, -64(%ebp) movl $L6, -56(%ebp) movl %esp, -52(%ebp) call __Unwind_SjLj_Register movl $1, -88(%ebp) leal -93(%ebp), %eax movl %eax, (%esp) call __ZN1DC1Ev L1: leal -40(%ebp), %eax movl %eax, (%esp) call __ZN1CD1Ev leal -92(%ebp), %eax movl %eax, (%esp) call __Unwind_SjLj_Unregister movl -12(%ebp), %ebx movl -8(%ebp), %esi movl -4(%ebp), %edi movl %ebp, %esp popl %ebp ret .p2align 4,,7 L6: L2: L4: addl $24, %ebp movl -84(%ebp), %eax movl %eax, -100(%ebp) leal -40(%ebp), %eax movl %eax, (%esp) call __ZN1CD1Ev movl $-1, -88(%ebp) movl -100(%ebp), %eax movl %eax, (%esp) call __Unwind_SjLj_Resume
コンストラクタ中で例外が起きた場合は対応するデストラクタを呼び出さないお約束になっているので、一番最初に作成するオブジェクトのコンストラクタは例外を投げても、
class C { public: C(); // 例外を投げるかも }; class D { public: ~D() throw(); }; void foo() { C c; // 例外を投げるかも D d; }
このとおり。
__Z3foov: L2: pushl %ebp movl %esp, %ebp leal -25(%ebp), %eax subl $56, %esp movl %eax, (%esp) call __ZN1CC1Ev leal -24(%ebp), %eax movl %eax, (%esp) call __ZN1DD1Ev leave ret
デストラクタについても同様のことが言えて、一番最後に実行されるデストラクタが例外を投げても、スタック巻き戻しコードは生成されない。 デストラクタはコンストラクタの逆の順序で実行されるので、要するに一番最初に構築されるオブジェクトはコンストラクタ・デストラクタが例外を投げても、
class C { public: C(); // 例外を投げるかも ~C(); // 例外を投げるかも }; class D { public: ~D() throw(); }; void foo() { C c; // c.C() が実行される・・・例外を投げるかも D d; // d.~D() が実行される // c.~C() が実行される・・・例外を投げるかも }
こんな感じで済む。
__Z3foov: L2: L4: pushl %ebp movl %esp, %ebp pushl %ebx subl $52, %esp leal -24(%ebp), %ebx movl %ebx, (%esp) call __ZN1CC1Ev leal -40(%ebp), %eax movl %eax, (%esp) call __ZN1DD1Ev movl %ebx, (%esp) call __ZN1CD1Ev addl $52, %esp popl %ebx popl %ebp ret
try ... catch で例外を捕捉すると、実行すべきデストラクタがなくても、
void bar(); void foo() { try { bar(); } catch (...) { } }
長い。
__Z3foov: pushl %ebp movl %esp, %ebp leal -12(%ebp), %eax subl $88, %esp movl %eax, -32(%ebp) leal -64(%ebp), %eax movl %ebx, -12(%ebp) movl %esi, -8(%ebp) movl %edi, -4(%ebp) movl $___gxx_personality_sj0, -40(%ebp) movl $LLSDA2, -36(%ebp) movl $L7, -28(%ebp) movl %esp, -24(%ebp) movl %eax, (%esp) call __Unwind_SjLj_Register movl $1, -60(%ebp) call __Z3barv L1: leal -64(%ebp), %eax movl %eax, (%esp) call __Unwind_SjLj_Unregister movl -12(%ebp), %ebx movl -8(%ebp), %esi movl -4(%ebp), %edi movl %ebp, %esp popl %ebp ret .p2align 4,,7 L7: L3: L4: addl $12, %ebp movl -56(%ebp), %eax movl %eax, (%esp) call ___cxa_begin_catch movl $-1, -60(%ebp) call ___cxa_end_catch leal -64(%ebp), %eax movl %eax, (%esp) call __Unwind_SjLj_Unregister movl -12(%ebp), %ebx movl -8(%ebp), %esi movl -4(%ebp), %edi movl %ebp, %esp popl %ebp ret
でも、tryの中で例外が出なければ、
void bar() throw(); void foo() { try { bar(); // 実は例外は発生しない } catch (...) { } }
短くなる。
__Z3foov: pushl %ebp movl %esp, %ebp popl %ebp jmp __Z3barv
まあ、最初から例外が出ないと分かっているなら、try ... catchはまったく意味ないけど。ちなみに、この例では最後のcall/retがjmpに置き換えられてる。
たいていの標準クラスは例外を投げる、かもしれない。
#include <vector> class C { public: ~C(); }; void foo(std::vector<int> &rv) { C c; rv.push_back(1); // メモリが足りないと例外を投げる }
__Z3fooRSt6vectorIiSaIiEE: pushl %ebp movl %esp, %ebp subl $120, %esp movl %esp, -52(%ebp) movl 8(%ebp), %eax movl %ebx, -12(%ebp) movl %esi, -8(%ebp) movl %eax, -100(%ebp) leal -24(%ebp), %eax movl %eax, -60(%ebp) leal -92(%ebp), %eax movl %eax, (%esp) movl %edi, -4(%ebp) movl $___gxx_personality_sj0, -68(%ebp) movl $LLSDA445, -64(%ebp) movl $L117, -56(%ebp) call __Unwind_SjLj_Register movl $1, -96(%ebp) movl -100(%ebp), %eax movl 4(%eax), %edx cmpl 8(%eax), %edx je L100 L102: testl %edx, %edx jne L118 L106: L108: movl -100(%ebp), %eax addl $4, 4(%eax) L114: L99: movl $-1, -88(%ebp) leal -40(%ebp), %eax movl %eax, (%esp) call __ZN1CD1Ev leal -92(%ebp), %eax movl %eax, (%esp) call __Unwind_SjLj_Unregister movl -12(%ebp), %ebx movl -8(%ebp), %esi movl -4(%ebp), %edi movl %ebp, %esp popl %ebp ret
が、std::auto_ptrはほとんどのメンバ関数が例外を投げない。
#include <memory> class C { public: ~C(); }; void bar(int *) throw(); // 例外は投げない void foo(std::auto_ptr<int> &rv) { C c; bar(rv.get()); // 例外は投げない }
__Z3fooRSt8auto_ptrIiE: L3: L5: L7: pushl %ebp movl %esp, %ebp subl $40, %esp movl 8(%ebp), %eax movl (%eax), %eax movl %eax, (%esp) call __Z3barPi leal -24(%ebp), %eax movl %eax, (%esp) call __ZN1CD1Ev leave ret
でもg++の場合、デストラクタだけは例外を投げるかもしれない。 ヘッダファイルに書いてあるコメントを見ると、故意にそうしてあるようだ。 具体的には、auto_ptrのテンプレートパラメータに指定したクラスのデストラクタが例外を投げる可能性があると、
#include <memory> class D { public: ~D(); // 例外を投げるかも。 }; class C { public: ~C() throw(); }; void foo() { C c; std::auto_ptr<D> ap; // ap.~auto_ptr() は例外を投げるかもしれない。 }
こんなん出てきます。
__Z3foov: L2: L5: L16: L12: L14: L1: pushl %ebp movl %esp, %ebp leal -24(%ebp), %eax subl $136, %esp movl %eax, -76(%ebp) leal -108(%ebp), %eax movl %ebx, -12(%ebp) movl %esi, -8(%ebp) movl %edi, -4(%ebp) movl %esp, -68(%ebp) movl %eax, (%esp) movl $___gxx_personality_sj0, -84(%ebp) movl $LLSDA402, -80(%ebp) movl $L16, -72(%ebp) call __Unwind_SjLj_Register movl $0, -56(%ebp) leal -40(%ebp), %eax movl %eax, (%esp) call __ZN1CD1Ev leal -108(%ebp), %eax movl %eax, (%esp) call __Unwind_SjLj_Unregister movl -12(%ebp), %ebx movl -8(%ebp), %esi movl -4(%ebp), %edi movl %ebp, %esp popl %ebp ret
デストラクタが例外を投げないと分かっていれば、インライン展開されて、
#include <memory> class D { public: D(); // コンストラクタは関係ない ~D() throw(); }; class C { public: ~C(); }; void foo() { C c; std::auto_ptr<D> ap; }
すっきりしてしまう。
__Z3foov: L2: L5: L12: L14: pushl %ebp movl %esp, %ebp leal -24(%ebp), %eax subl $56, %esp movl %eax, (%esp) movl $0, -40(%ebp) call __ZN1CD1Ev leave ret
例外を投げない、と指定した関数から例外が投げられると、unexpected関数が呼ばれ、この関数はデフォルトではterminateを呼び出し、さらにabortなどが呼ばれてそこでプログラムの実行が終了する。 例えば、このプログラムは、
#include <iostream> void bar() { std::cout << "bar{ "; throw 1; std::cout << "}bar "; } void foo() { std::cout << "foo{ "; bar(); std::cout << "}foo "; } int main() { std::cout << "main{ "; try { std::cout << "try{ "; foo(); std::cout << "}try "; } catch (...) { std::cout << "CATCH! "; } std::cout << "}main\n"; return 0; }
main{ try{ foo{ bar{ CATCH! }mainこんな感じで普通に終了するが、fooは例外を投げない! と指定すると、
#include <iostream> void bar() { std::cout << "bar{ "; throw 1; std::cout << "}bar "; } void foo() throw() { std::cout << "foo{ "; bar(); std::cout << "}foo "; } int main() { std::cout << "main{ "; try { std::cout << "try{ "; foo(); std::cout << "}try "; } catch (...) { std::cout << "CATCH! "; } std::cout << "}main\n"; return 0; }
main{ try{ foo{ bar{ 34 [sig] a 3488 _cygtls::handle_exceptions: Error while dumping state (probably corrupted stack) Segmentation fault (core dumped)
コアをお吐きになります。 ちなみに、関数fooのアセンブラソースは、throw()がなければ、
__Z3foov: pushl %ebp movl $LC2, %eax movl %esp, %ebp subl $8, %esp movl %eax, 4(%esp) movl $__ZSt4cout, (%esp) call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc call __Z3barv movl $__ZSt4cout, (%esp) movl $LC3, %eax movl %eax, 4(%esp) call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc leave ret
と短く済むが、throw()を付けると、
__Z3foov: pushl %ebp movl %esp, %ebp leal -12(%ebp), %eax subl $88, %esp movl %eax, -32(%ebp) leal -64(%ebp), %eax movl %eax, (%esp) movl %ebx, -12(%ebp) movl %esi, -8(%ebp) movl %edi, -4(%ebp) movl $___gxx_personality_sj0, -40(%ebp) movl $LLSDA1381, -36(%ebp) movl $L8, -28(%ebp) movl %esp, -24(%ebp) call __Unwind_SjLj_Register movl $__ZSt4cout, (%esp) movl $LC2, %eax movl %eax, 4(%esp) movl $1, -60(%ebp) call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc call __Z3barv movl $__ZSt4cout, (%esp) movl $LC3, %eax movl %eax, 4(%esp) call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc L4: leal -64(%ebp), %eax movl %eax, (%esp) call __Unwind_SjLj_Unregister movl -12(%ebp), %ebx movl -8(%ebp), %esi movl -4(%ebp), %edi movl %ebp, %esp popl %ebp ret .p2align 4,,7 L8: addl $12, %ebp cmpl $-1, -52(%ebp) movl -56(%ebp), %eax je L5 movl %eax, (%esp) movl $-1, -60(%ebp) call __Unwind_SjLj_Resume .p2align 4,,7 L5: movl %eax, (%esp) movl $-1, -60(%ebp) call ___cxa_call_unexpected
と、スタック巻き戻しコードが入る。どうもfooで例外をフックして、cxa_call_unexpectedを呼び出しているようだ。 unexpectedはset_unexpectedで挙動を変えることができるが、そのためのラッパがcxa_call_unexpectedなのだろう。
いちいちthrow()を書くのがめんどうだ、という向きには、-fno-exceptions というオプションがある。 このオプションはg++のものだが、他のコンパイラにも同様のオプションがあるようだ。 MSVCの場合、少なくともMSVS.2003までは、何も指定しない=スタック巻き戻しを行わない、だった。
例えば、冒頭に挙げたコードを、g++ -O2 -fno-exceptions -S でコンパイルすると、throw()を書いたのと同じコードになる。
__Z3foov: pushl %ebp movl %esp, %ebp subl $40, %esp call __Z3barv leal -24(%ebp), %eax movl %eax, (%esp) call __ZN1CD1Ev leave ret
-fno-exceptionsを付けた部分ではthrow・catchが使えない。
void foo() { throw 1; }
$ g++ -O2 -fno-exceptions -c xcp.cpp xcp2.cpp: In function `void foo()': xcp2.cpp:3: error: exception handling disabled, use -fexceptions to enable
-fno-exceptionsを付けたコードと、付けないコードは一応共存できるようだ。
// xcp2.cpp: -fno-exceptions でコンパイル #include <iostream> void bar(); // 例外を投げるかもしれない void foo() throw() // 例外は投げない? { std::cout << "foo{ "; bar(); // いや、やっぱり投げるのか?! std::cout << "}foo "; }
// xcp.cpp: 普通にコンパイル #include <iostream> void bar() { std::cout << "bar{ "; throw(1); // 投げちゃった。 std::cout << "}bar "; } void foo() throw(); int main() { std::cout << "main{ "; try { std::cout << "try{ "; foo(); std::cout << "}try "; } catch (...) { std::cout << "CATCH! "; } std::cout << "}main\n"; }
例外を出さないはずの先ほどの例と違って、unexpectedは呼ばれず、普通に終了する。
$ g++ -c -O2 -fno-exceptions xcp2.cpp $ g++ -O2 -o xcp xcp.cpp xcp2.o $ ./xcp main{ try{ foo{ bar{ CATCH! }main
ただし、xcp2.cppでは一切の例外コードが出力されない。 スタック巻き戻しコードも生成されないので、例外が起きた場合にxcp2.cpp内のデストラクタが呼ばれなくなる。
// xcp2.cpp: -fno-exceptions でコンパイル #include <iostream> void bar(); class D { public: D() throw() { std::cout << "D::D() "; } ~D() throw() { std::cout << "D::~D() "; } }; void foo() { std::cout << "foo{ "; D d; bar(); std::cout << "}foo "; }
// xcp.cpp: 普通にコンパイル #include <iostream> class C { public: C(const char *s) throw() : ms(s) { std::cout << ms << "::C() "; } ~C() throw() { std::cout << ms << "::~C() "; } const char *ms; }; void bar() { std::cout << "bar{ "; C c1("c1"); throw(1); std::cout << "}bar "; } void foo(); int main() { std::cout << "main{ "; try { std::cout << "try{ "; C c2("c2"); foo(); std::cout << "}try "; } catch (...) { std::cout << "CATCH! "; } std::cout << "}main\n"; }
こんなコードを以下のようにコンパイルして実行すると、
$ g++ -c -O2 -fno-exceptions xcp2.cpp $ g++ -O2 -o xcp xcp.cpp xcp2.o $ ./xcp main{ try{ c2::C() foo{ D::D() bar{ c1::C() c1::~C() c2::~C() CATCH! }main
Dのコンストラクタに対応するデストラクタが実行されていないことが分かる。
仮想関数に例外指定をした場合、派生クラスでオーバーライドする関数は、それよりもより厳しい(少ない)例外指定しかできない。 だから、仮想関数を throw() にしてしまうと、派生クラスでオーバーライドする仮想関数も throw() にしなければならない。
class C { public: virtual void vf() throw(); }; class D : public C { public: virtual void vf(); };
xcp2.cpp:8: error: looser throw specifier for `virtual void D::vf()' xcp2.cpp:3: error: overriding `virtual void C::vf() throw ()'
これは警告ではなくエラーである。 なので、仮想関数は例外指定はしない方がよさそうだ。
実行速度に効いてくるループの一番内側などは、例外コードに細心の注意をはらう必要がある。 ループの一番内側から関数を呼び出す場合、その関数でデストラクタのあるオブジェクトを作ってから破壊するまでの間に、例外が発生する可能性のある関数を呼んでしまうと、一番内側のループが一回まわるごとにスタック巻き戻しコードが実行されてしまう。 例えば、以下のコードはものすごく損である。
class C { public: C(); ~C(); }; void bar(); void foo() { C c; bar(); } void hoge() { for (int i = 0; i < 10000; i++) foo(); }
__Z3foov: pushl %ebp movl %esp, %ebp leal -24(%ebp), %eax subl $120, %esp movl %eax, -60(%ebp) leal -92(%ebp), %eax movl %eax, (%esp) movl %ebx, -12(%ebp) movl %esi, -8(%ebp) movl %edi, -4(%ebp) movl $___gxx_personality_sj0, -68(%ebp) movl $LLSDA2, -64(%ebp) movl $L6, -56(%ebp) movl %esp, -52(%ebp) call __Unwind_SjLj_Register movl $-1, -88(%ebp) leal -40(%ebp), %eax movl %eax, (%esp) call __ZN1CC1Ev movl $1, -88(%ebp) call __Z3barv L1: movl $-1, -88(%ebp) leal -40(%ebp), %eax movl %eax, (%esp) call __ZN1CD1Ev leal -92(%ebp), %eax movl %eax, (%esp) call __Unwind_SjLj_Unregister movl -12(%ebp), %ebx movl -8(%ebp), %esi movl -4(%ebp), %edi movl %ebp, %esp popl %ebp ret ----- 8< ----- 8< ----- 中略 ----- 8< ----- 8< ----- __Z4hogev: pushl %ebp movl %esp, %ebp pushl %ebx subl $4, %esp movl $9999, %ebx .p2align 4,,15 L12: call __Z3foov decl %ebx jns L12 popl %eax popl %ebx popl %ebp ret
「中略」の上が関数fooだが、この中には分岐命令は入っていないので、fooが呼ばれるたびにこのコードが全部実行される。 つまり、こんなデカいものが10,000回実行される。 これを、barが例外を投げないようにすると、
class C { public: C(); ~C(); }; void bar() throw(); void foo() { C c; bar(); } void hoge() { for (int i = 0; i < 10000; i++) foo(); }
__Z3foov: L2: pushl %ebp movl %esp, %ebp pushl %ebx subl $36, %esp leal -24(%ebp), %ebx movl %ebx, (%esp) call __ZN1CC1Ev call __Z3barv movl %ebx, (%esp) call __ZN1CD1Ev addl $36, %esp popl %ebx popl %ebp ret ----- 8< ----- 8< ----- 中略 ----- 8< ----- 8< ----- __Z4hogev: pushl %ebp movl %esp, %ebp pushl %ebx subl $4, %esp movl $9999, %ebx .p2align 4,,15 L8: call __Z3foov decl %ebx jns L8 popl %eax popl %ebx popl %ebp ret
ほぼ必要なコードだけになる。 あるいは、クラスCのデストラクタをなくしてもよい。
class C { public: C(); // デストラクタを削除 }; void bar(); // 例外を投げるかもしれない void foo() { C c; bar(); } void hoge() { for (int i = 0; i < 10000; i++) foo(); }
__Z3foov: pushl %ebp movl %esp, %ebp leal -1(%ebp), %eax subl $8, %esp movl %eax, (%esp) call __ZN1CC1Ev call __Z3barv leave ret ----- 8< ----- 8< ----- 中略 ----- 8< ----- 8< ----- __Z4hogev: pushl %ebp movl %esp, %ebp pushl %ebx subl $4, %esp movl $9999, %ebx .p2align 4,,15 L6: call __Z3foov decl %ebx jns L6 popl %eax popl %ebx popl %ebp ret
cの生成とbarの呼び出しを逆にする、という手もある。
class C { public: C(); ~C(); }; void bar(); void foo() { bar(); C c; } void hoge() { for (int i = 0; i < 10000; i++) foo(); }
__Z3foov: L2: pushl %ebp movl %esp, %ebp pushl %ebx subl $36, %esp leal -24(%ebp), %ebx call __Z3barv movl %ebx, (%esp) call __ZN1CC1Ev movl %ebx, (%esp) call __ZN1CD1Ev addl $36, %esp popl %ebx popl %ebp ret ----- 8< ----- 8< ----- 中略 ----- 8< ----- 8< ----- __Z4hogev: pushl %ebp movl %esp, %ebp pushl %ebx subl $4, %esp movl $9999, %ebx .p2align 4,,15 L8: call __Z3foov decl %ebx jns L8 popl %eax popl %ebx popl %ebp ret
どれもできない場合、関数の規模が小さければインラインにしてしまうという手もある。
class C { public: C(); ~C(); }; void bar(); inline void foo() { C c; bar(); } void hoge() { for (int i = 0; i < 10000; i++) foo(); }
__Z4hogev: pushl %ebp movl %esp, %ebp leal -24(%ebp), %eax pushl %edi pushl %esi pushl %ebx subl $108, %esp movl %eax, -60(%ebp) leal -92(%ebp), %eax movl $___gxx_personality_sj0, -68(%ebp) movl $LLSDA3, -64(%ebp) movl $L11, -56(%ebp) movl %esp, -52(%ebp) movl %eax, (%esp) call __Unwind_SjLj_Register movl $0, -96(%ebp) L10: movl $-1, -88(%ebp) leal -40(%ebp), %eax movl %eax, (%esp) call __ZN1CC1Ev movl $1, -88(%ebp) call __Z3barv movl $-1, -88(%ebp) leal -40(%ebp), %eax movl %eax, (%esp) call __ZN1CD1Ev incl -96(%ebp) cmpl $9999, -96(%ebp) jle L10 L1: leal -92(%ebp), %eax movl %eax, (%esp) call __Unwind_SjLj_Unregister addl $108, %esp popl %ebx popl %esi popl %edi popl %ebp ret
スタック巻き戻しのためのコードは生成されるが、ループはL10からL1の直前までなので、hogeの呼び出し1回につき、スタック巻き戻しコードは1回しか実行されない。 最初の例に比べてずっと速くなる。
浮動小数の数値演算関数については、問題が発生しても例外は投げず、nanとかinfとかが返ってくる、みたいだ。 整数の場合は問題があると落ちる場合がある。 一番分かりやすいのは「ゼロによる除算」だろう。
#include <stdlib.h> #include <iostream> int main(int argc, char *argv[]) { std::cout << 1000 / atof(argv[1]) << "\n"; return 0; }
このプログラムをコマンドライン引数に0を与えて実行すると、
inf
と表示されるが、atof を atoi にすると、「ゼロによる除算」の例外で落ちる。 これを引っ掛けられるかどうかは不明だが、例外で引っ掛けるより割り算する前に除数がゼロでないことを調べる方がはるかに楽なので、割り算を見たら分母がゼロになる可能性がないかをまず疑うクセを付けておくとよい。
まあしかし、浮動小数の場合でも、ライブラリによっては例外を投げるかもしれないので、除算以外にも、sqrt・log・acosなど、定義域が制限されている関数を使う場合には、関数を呼び出す前に引数を確認するクセを付けたほうがよい。 top
Copyright (C) 2008-2011 You SUZUKI
$Id: exception.htm,v 1.8 2011/04/09 15:24:39 you Exp $