web-dev-qa-db-ja.com

静的関数変数へのアクセスは、グローバル変数へのアクセスよりも遅いですか?

静的ローカル変数 は最初の関数呼び出しで初期化されます:

ブロックスコープで宣言子staticを使用して宣言された変数は、静的な保存期間を持ちますが、制御が宣言を最初に通過するときに初期化されます(初期化がゼロまたは定数の初期化である場合は、ブロックが最初に入力される前に実行できます)。それ以降のすべての呼び出しでは、宣言はスキップされます。

また、C++ 11にはさらに多くのチェックがあります。

複数のスレッドが同じ静的ローカル変数を同時に初期化しようとすると、初期化は1回だけ発生します(std :: call_onceを使用して任意の関数で同様の動作を取得できます)。注:この機能の通常の実装では、ダブルチェックされたロックパターンのバリアントを使用します。これにより、既に初期化されたローカルスタティックのランタイムオーバーヘッドが単一の非アトミックブール比較に削減されます。 (C++ 11以降)

同時に、 グローバル変数はプログラムの開始時に初期化されるようです (ただし、技術的にはallocation/deallocationはcppreferenceに記載されています):

静的ストレージ期間。オブジェクトのストレージは、プログラムの開始時に割り当てられ、プログラムの終了時に割り当て解除されます。オブジェクトのインスタンスは1つだけ存在します。ネームスペーススコープ(グローバルネームスペースを含む)で宣言されたすべてのオブジェクトには、このストレージ期間に加えて、staticまたはexternで宣言されたオブジェクトがあります。

したがって、次の例を考えます。

_struct A {
    // complex type...
};
const A& f()
{
    static A local{};
    return local;
}

A global{};
const A& g()
{
    return global;
}
_

f()は、呼び出されるたびに変数が初期化されたかどうかを確認する必要があり、したがってf()g()よりも遅いと仮定するのは正しいですか?

25
Dev Null

もちろん概念的には正しいですが、現代のアーキテクチャはこれに対処できます。

最新のコンパイラとアーキテクチャは、既に初期化されたブランチが想定されるようにパイプラインを配置します。したがって、初期化のオーバーヘッドにより、余分なパイプラインダンプが発生します。

疑問がある場合は、アセンブリを確認してください。

15
Bathsheba

はい、ほぼ確実に少し遅くなります。ただし、ほとんどの場合、それは重要ではなく、「ロジックとスタイル」の利点によってコストが上回ります。

技術的には、関数ローカルな静的変数はグローバル変数と同じです。その名前だけがnotグローバルに知られている(これは良いことです)だけであり、その初期化は正確に指定された時間だけでなく、一度だけで、スレッドセーフであることが保証されています。

これは、関数ローカルの静的変数mustが初期化が行われたかどうかを知っているため、少なくとも1つの追加メモリアクセスと、グローバル(原則)で不要な条件付きジャンプが1つ必要であることを意味します。実装mayはグローバルに対して同様の処理を行いますが、必要ではない(通常は必要ありません)。

2つを除くすべての場合にジャンプが正しく予測される可能性があります。最初の2つの呼び出しは、間違って予測される可能性が非常に高い(通常、ジャンプは、デフォルトではなく、最初の呼び出しの誤った仮定で行われると想定され、後続のジャンプは最後の呼び出しと同じパスをたどり、再び間違っていると想定されます)。その後は、100%正しい予測に近づいてください。
しかし、正しく予測されたジャンプでさえフリーではありません(CPUは、完了までにゼロ時間がかかるとしても、サイクルごとに所定の数の命令のみを開始できます)が、それほどではありません。メモリレイテンシ(最悪の場合は数百サイクルになる可能性があります)を正常に非表示にできる場合、パイプライン処理でコストほぼがなくなります。また、すべてのアクセスは、他の方法では必要のない追加のキャッシュラインをフェッチします(has-been-initializedフラグは、データと同じキャッシュラインに格納されない可能性があります)。したがって、L1のパフォーマンスはわずかに低下します(L2は「ええ、だから何」と言えるくらいの大きさでなければなりません)。

また、実際には何かを実行する必要があります一度とスレッドセーフグローバル(原則的に)する必要はありません、少なくともあなたが見る方法では。実装は別のことを行うことができますが、ほとんどの場合、mainに入る前にグローバルを初期化するだけで、変数のほとんどがmemsetで行われることもあります。とにかくゼロ。
静的変数must初期化コードの実行時に初期化され、スレッドセーフな方法で発生する必要があります。実装がどれだけ無駄になるかにもよりますが、これは非常に高価になる可能性があります。スレッドセーフ機能を放棄し、常にfno-threadsafe-statics(これが標準に準拠していない場合でも)GCC(それ以外の場合はOKのオールラウンドコンパイラー)がすべての静的初期化に対してmutexを実際にロックすることを発見した後。

6
Damon

から https://en.cppreference.com/w/cpp/language/initialization

遅延動的初期化
動的初期化は、メイン関数の最初のステートメントの前(静的)またはスレッドの初期関数(スレッドローカル)の前に発生するか、後で発生するかを実装定義します。

非インライン変数の初期化(C++ 17以降)がmain/thread関数の最初のステートメントの後に発生するように延期される場合、静的/スレッドストレージ期間が定義された変数の最初のodr使用の前に発生します初期化される変数と同じ変換単位。

そのため、グローバル変数に対しても同様のチェックmayを行う必要があります。

そのため、f()は必要ありません "_"g()よりも遅い。

2
Jarod42

g()はスレッドセーフではなく、あらゆる種類の順序付けの問題の影響を受けやすくなっています。安全性は犠牲になります。支払う方法はいくつかあります。

Meyer's Singletonであるf()は、すべてのアクセスで料金を支払います。頻繁にアクセスする場合、またはコードのパフォーマンスに敏感なセクションでアクセスする場合は、f()を避けるのが理にかなっています。プロセッサには、分岐予測専用の回路がおそらくあるため、分岐前にアトミック変数を読み取る必要があります。初期化が一度だけ行われたことを保証するだけで継続的に支払うのは、高額です。

後述のh()は、g()と非常によく似た動作をしますが、h_init()は実行の開始時に1回だけ呼び出されることを前提としています。できれば、main()の行として呼び出されるサブルーチンを定義することをお勧めします。 h_init()のようなすべての関数を絶対順序で呼び出します。うまくいけば、これらのオブジェクトを破壊する必要はありません。

あるいは、GCCを使用する場合は、h_init()__attribute__((constructor))注釈を付けることができます。ただし、静的initサブルーチンの明示性を好みます。

_A * h_global = nullptr;
void h_init() { h_global = new A { }; }
A const& h() { return *h_global; }
_

h2()h()に似ていますが、余分な間接性を除いたものです。

_alignas(alignof(A)) char h2_global [sizeof(A)] = { };
void h2_init() { new (std::begin(h2_global)) A { }; }
A const& h2() { return * reinterpret_cast <A const *> (std::cbegin(h2_global)); }
_
0
KevinZ