次の ドラフト スコットマイヤーズの新しいC++ 11の本から(2ページ、7〜21行目)
コールスタックの巻き戻しと場合によっては巻き戻しの違いは、コード生成に驚くほど大きな影響を与えます。 noexcept関数では、オプティマイザーは、例外が関数から伝播する場合にランタイムスタックを巻き戻し可能な状態に保つ必要はありません。また、例外が関数を離れた場合に、noexcept関数のオブジェクトが構築の逆順序で破棄されるようにする必要もありません。 。その結果、noexcept関数の本体内だけでなく、関数が呼び出されるサイトでも最適化の機会が増えます。このような柔軟性は、noexcept関数にのみ存在します。 「throw()」例外仕様のある関数は、例外仕様のない関数と同様に、それを欠いています。
対照的に、 "C++パフォーマンスに関するテクニカルレポート" のセクション_5.4
_では、例外処理を実装する「コード」と「テーブル」の方法について説明しています。特に、「テーブル」メソッドは、例外がスローされず、スペースオーバーヘッドしかない場合、時間オーバーヘッドがないことが示されています。
私の質問はこれです-スコット・マイヤーズが巻き戻しとおそらく巻き戻しについて話すとき、どのような最適化について話しているのですか?これらの最適化がthrow()
に適用されないのはなぜですか?彼のコメントは、2006年のTRで言及されている「コード」方式にのみ適用されますか?
「no」オーバーヘッドがあり、次にnoオーバーヘッドがあります。コンパイラはさまざまな方法で考えることができます。
TRは、スローが発生しない限りアクションを実行する必要がないため、テーブル駆動型アプローチにはオーバーヘッドがないと述べています。非例外的な実行パスは単純に進みます。
ただし、テーブルを機能させるには、例外ではないコードに追加の制約が必要です。各オブジェクトは、例外がその破壊につながる前に完全に初期化する必要があり、スローする可能性のある呼び出し全体での命令の並べ替え(インラインコンストラクターからなど)を制限します。同様に、オブジェクトは、その後の例外が発生する前に完全に破棄する必要があります。
テーブルベースのアンワインドは、スタックフレームを使用したABI呼び出し規約に従った関数でのみ機能します。例外の可能性がなければ、コンパイラはABIを無視して、フレームを自由に省略できた可能性があります。
テーブルと個別の例外的なコードパスの形式のスペースオーバーヘッド、別名膨張は、実行時間に影響を与えない可能性がありますが、プログラムをダウンロードしてRAMにロードするのにかかる時間には影響を与える可能性があります。
それはすべて相対的ですが、noexcept
はコンパイラーをいくらか緩めます。
noexcept
とthrow()
の違いは、throw()
の場合でも例外スタックがアンワインドされ、デストラクタが呼び出されるため、実装はスタックを追跡する必要があることです(標準の15.5.2 The std::unexpected() function
を参照)。
それどころか、std::terminate()
はスタックをほどく必要はありません(_15.5.1
_はstd::terminate()
が呼び出される前にスタックがほどかれるかどうかは実装定義であると述べています) 。
GCCは実際にはnoexcept
のスタックを巻き戻さないようです: デモ
clangがまだほどけている間: デモ
(デモでf_noexcept()
にコメントし、f_emptythrow()
のコメントを解除して、throw()
のGCCとclangの両方がスタックを巻き戻すことを確認できます)
次の例を見てください。
#include <stdio.h>
int fun(int a) {
int res;
try
{
res = a *11;
if(res == 33)
throw 20;
}
catch (int e)
{
char *msg = "error";
printf(msg);
}
return res;
}
int main(int argc, char** argv) {
return fun(argc);
}
入力として渡されるデータは、コンパイラの観点からは予測できないため、-O3
の最適化を行っても、呼び出しまたは例外システムを完全に排除することはできません。
LLVM IRでは、fun
関数は大まかに次のように変換されます。
define i32 @_Z3funi(i32 %a) #0 {
entry:
%mul = mul nsw i32 %a, 11 // The actual processing
%cmp = icmp eq i32 %mul, 33
br i1 %cmp, label %if.then, label %try.cont // jump if res == 33 to if.then
if.then: // lots of stuff happen here..
%exception = tail call i8* @__cxa_allocate_exception(i64 4) #3
%0 = bitcast i8* %exception to i32*
store i32 20, i32* %0, align 4, !tbaa !1
invoke void @__cxa_throw(i8* %exception, i8* bitcast (i8** @_ZTIi to i8*), i8* null) #4
to label %unreachable unwind label %lpad
lpad:
%1 = landingpad { i8*, i32 } personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*)
catch i8* bitcast (i8** @_ZTIi to i8*)
... // also here..
invoke.cont:
... // and here
br label %try.cont
try.cont: // This is where the normal flow should go
ret i32 %mul
eh.resume:
resume { i8*, i32 } %1
unreachable:
unreachable
}
ご覧のとおり、コードパスは、通常の制御フロー(例外なし)の場合は単純ですが、同じ関数内のいくつかの基本ブロックブランチで構成されています。
確かに、実行時にほぼコストは関連付けられません使用した分だけ支払う(スローしない場合、余分なことは何も起こりません)が、複数のブランチがあると、あなたのパフォーマンスも傷つけます、例えば.
そして確かに、通常の制御フローとランディングパッド/例外エントリポイントの間でパススルーブランチの最適化を実行することはできません。
例外は複雑なメカニズムであり、noexcept
は、コストがゼロのEHであっても、コンパイラーの寿命を大幅に延ばします。
編集:noexcept
指定子の特定のケースで、コンパイラーがコードがスローしないことを '証明'できない場合、std::terminate
EHが設定されます(実装に依存する詳細を含む)。どちらの場合も(コードがスローされない、および/またはコードがスローされないことを証明できない)、関連するメカニズムはより単純であり、コンパイラーの制約は少なくなります。とにかく、最適化の理由で実際にはnoexcept
を使用しませんが、これは重要なセマンティック指標でもあります。
さまざまなテストケースについて、「noexcept」指定子を追加した場合のパフォーマンス効果を測定するためのベンチマークを作成しました。 https://github.com/N-Dekker/noexcept_benchmark 特定のテストケースがあります'noexcept'を使用して、スタックの巻き戻しをスキップする可能性を利用できます。
void recursive_func(recursion_data& data) noexcept // or no 'noexcept'!
{
if (--data.number_of_func_calls_to_do > 0)
{
noexcept_benchmark::throw_exception_if(data.volatile_false);
object_class stack_object(data.object_counter);
recursive_func(data);
}
}
https://github.com/N-Dekker/noexcept_benchmark/blob/v03/lib/stack_unwinding_test.cpp#L48
ベンチマークの結果を見ると、この特定のテストケースでは、VS2017x64とGCC5.4.0の両方で、「noexcept」を追加することでパフォーマンスが大幅に向上しているようです。