11月16日更新
この質問はもともと 2012 年に「娯楽」で回答されたものであり、決定的な回答を提供するとは主張しておらず、その旨の警告がありました。後から考えると、そのような娯楽はおそらく非公開のままにしておかなければなりませんでしたが、楽しんだ人もいました.
2016 年 8 月にこの Q&A に注目し、適切な回答を提供しました。その中で次のように書いています。
私は一見グレッグ・パーカーに反対するつもりですが、おそらくそうではありません...
グレッグと私は、私たちが同意しないかどうか、または答え、または何かについて意見が一致していないようです;-)したがって、2016年8月の回答を、回答のより詳細な根拠、なぜ間違っている可能性があるのか 、もしそうならどのように更新しますそれを修正します(したがって、元の質問に対する答えはまだ「はい」です)。うまくいけば、グレッグと私は同意するか、何かを学ぶことになるでしょう - どちらの結果も良いです!
最初に8月16日の回答をそのままにして、次に回答の根拠を説明します。混乱を避けるために、元の娯楽は削除されました。歴史の学生は編集証跡を表示できます。
回答: 2016 年 8 月
私は一見グレッグ・パーカーに反対するつもりですが、おそらくそうではありません...
元の質問:
dispatch_once_t
述語を静的変数ではなくメンバー変数として宣言できますか?
簡単な回答:オブジェクトの最初の作成と の使用の間にメモリ バリアがある場合、答えは「はい」ですdispatch_once
。
簡単な説明:dispatch_once_t
変数の要件は、dispatch_once
最初はゼロでなければならないということです。難しいのは、最新のマルチプロセッサでのメモリの並べ替え操作です。プログラム テキスト (高水準言語またはアセンブラー レベル) に従って場所へのストアが実行されたように見える場合でも、実際のストアは並べ替えられ、同じ場所の後続の読み取り後に発生する場合があります。このメモリバリアに対処するために、メモリバリアを使用して、それらの前に発生したすべてのメモリ操作を、その後のメモリ操作よりも先に完了させることができます。Apple は、これを行うための を提供しOSMemoryBarrier()
ています。
Appleではdispatch_once
、ゼロで初期化されたグローバル変数はゼロであることが保証されていますが、ゼロで初期化されたインスタンス変数 (ここではゼロ初期化が Objective-C のデフォルトです)dispatch_once
は実行前にゼロであるとは保証されないと述べています。
解決策は、メモリ バリアを挿入することです。インスタンスのいくつかのメンバーメソッドで発生すると仮定すると、dispatch_once
このメモリバリアを置く明白な場所は、init
(1) (インスタンスごとに) 1 回だけ実行され、(2)init
他のメンバーの前に返される必要があるため、メソッド内にあります。メソッドを呼び出すことができます。
はい、適切なメモリバリアがdispatch_once
あれば、インスタンス変数で使用できます。
2016年11月
前文: に関する注意事項dispatch_once
これらのメモは、Apple のコードとコメントに基づいています dispatch_once
。
の使用法はdispatch_once
標準パターンに従います。
id cachedValue;
dispatch_once_t predicate = 0;
...
dispatch_once(&predicate, ^{ cachedValue = expensiveComputation(); });
... use cachedValue ...
最後の 2 行は、次のようにインライン(dispatch_once
はマクロ) に展開されます。
if (predicate != ~0) // (all 1's, indicates the block has been executed) [A]
{
dispatch_once_internal(&predicate, block); // [B]
}
... use cachedValue ... // [C]
ノート:
Apple のソースには、predicate
ゼロに初期化する必要があると記載されており、グローバル変数と静的変数のデフォルトは初期化ゼロに設定されていることに注意してください。
行 [A] にはメモリ バリアがないことに注意してください。投機的先読みと分岐述語を持つプロセッサではcachedValue
、行 [C] の読み取りが行 [A] の読み取りの前に発生し、間違った結果 ( の間違った値)predicate
につながる可能性があります。cachedValue
これを防ぐためにバリアを使用することもできますが、これは遅く、Apple は、1 回のブロックが既に実行されている一般的なケースでこれを高速にしたいと考えているため、...
dispatch_once_internal
、バリアとアトミック操作を内部で使用する行 [B] は、特殊なバリアを使用しdispatch_atomic_maximally_synchronizing_barrier()
て投機的な先読みを無効にし、行 [A] をバリアフリーにして高速にします。
前に行 [A] に到達したプロセッサdispatch_once_internal()
は実行されて変更され、からpredicate
読み取る必要があります。ゼロに初期化されたグローバルまたは静的を使用すると、これが保証されます。0
predicate
predicate
現在の目的のために重要なことは、行 [A] がバリアなしで機能するようにdispatch_once_internal
変異することです。 predicate
8月16日の回答の長い説明:
dispatch_once()
したがって、ゼロに初期化されたグローバルまたは静的を使用すると、のバリアフリー高速パスの要件を満たすことがわかります。dispatch_once_internal()
また、 toによって行われたpredicate
変更が正しく処理されることもわかっています。
決定する必要があるのは、インスタンス変数を使用しpredicate
て、上記の行 [A] で事前に初期化された値を読み取ることができないように初期化できるかどうかです。
私の8月16日の回答は、これが可能であると言います。この根拠を理解するには、投機的先読みを使用するマルチプロセッサ環境でのプログラムとデータ フローを考慮する必要があります。
8 月 16 日の回答の実行とデータ フローの概要は次のとおりです。
Processor 1 Processor 2
0. Call alloc
1. Zero instance var used for predicate
2. Return object ref from alloc
3. Call init passing object ref
4. Perform barrier
5. Return object ref from init
6. Store or send object ref somewhere
...
7. Obtain object ref
8. Call instance method passing obj ref
9. In called instance method dispatch_once
tests predicate, This read is dependent
on passed obj ref.
インスタンス変数を述語として使用できるようにするには、ステップ 1 でゼロにする前にメモリ内の値を読み取るような方法でステップ 9 を実行することは不可能でなければなりません。
ステップ 4 が省略された場合、つまり適切なバリアが挿入されていない場合init
、プロセッサ 2 はステップ 9 を実行する前にプロセッサ 1 によって生成されたオブジェクト参照の正しい値を取得する必要がありますが、(理論的には) プロセッサ 1 のゼロ書き込みが可能です。手順 1 ではまだ実行されておらず、グローバル メモリに書き込まれておらず、プロセッサ 2 にはそれらが表示されません。
そのため、ステップ 4 を挿入してバリアを実行します。
ただし、投機的な先読みを考慮する必要がdispatch_once()
あります。プロセッサ 2 は、ステップ 4 のバリアがメモリがゼロであることを確認する前に、ステップ 9 の読み取りを実行できますか?
検討:
プロセッサ 2 は、投機的またはその他の方法で、ステップ 7 で取得したオブジェクト参照を取得するまで、ステップ 9 の読み取りを実行できません。これを実行するには、投機的にプロセッサがステップ 8 のメソッド呼び出しを決定する必要があり、Objective-C での宛先は動的に決定され、ステップ 9 を含むメソッドに到達します。これは非常に高度な (しかし不可能ではない) 推測です。
ステップ 7 では、ステップ 6 でオブジェクト参照が格納/渡されるまで、オブジェクト参照を取得できません。
ステップ 6 は、ステップ 5 がそれを返すまで、それを保管/渡すことができません。と
ステップ5はステップ4の関門のあと…
TL;DR : バリアを含むステップ 4 の後まで、ステップ 9 で読み取りを実行するために必要なオブジェクト参照を取得するにはどうすればよいですか? (そして、実行パスが長く、複数の分岐があり、いくつかの条件付き (メソッド ディスパッチ内など) がある場合、投機的な先読みはまったく問題になるのでしょうか?)
したがって、ステップ 9 に影響を与える投機的な先読みが存在する場合でも、ステップ 4 の障壁は十分であると私は主張します。
グレッグのコメントの考察:
Greg は、述語に関する Apple のソース コードのコメントを「ゼロに初期化する必要がある」から「非ゼロであってはならない」へと強化しました。dispatch_once()
この議論は、バリアフリーの高速パスに必要な最新のプロセッサによる投機的な先読みを打ち負かすことに基づいています。
インスタンス変数はオブジェクトの作成時にゼロに初期化され、それらが占有するメモリはそれ以前はゼロではない可能性があります。ただし、上記で説明したように、適切なバリアを使用してdispatch_once()
、初期化前の値を読み取らないようにすることができます。私が彼のコメントに正しく従うならば、グレッグは私の主張に同意しないと思います。
Greg が正しいと仮定しましょう (まったくあり得ないことではありません!)。その場合、Apple が で既に対処した状況にdispatch_once()
あり、先読みを無効にする必要があります。Apple はdispatch_atomic_maximally_synchronizing_barrier()
バリアを使用してそれを行います。ステップ 4 でこの同じバリアを使用して、プロセッサ 2 による投機的先読みがすべて無効になるまで、次のコードが実行されないようにすることができます。次のコードのように、手順 5 と 6 は、プロセッサ 2 が手順 9 を投機的に実行するために使用できるオブジェクト参照を取得する前に実行する必要があります。
したがって、グレッグの懸念を理解すれば、使用dispatch_atomic_maximally_synchronizing_barrier()
することでそれらに対処でき、標準バリアの代わりに使用しても、実際には必要なくても問題は発生しません。したがって、それが必要であるとは確信していませんが、最悪の場合、そうすることは無害です。したがって、私の結論は以前のままです(強調を追加):
はい、適切なメモリバリアがdispatch_once
あれば、インスタンス変数で使用できます。
私の論理が間違っていたら、Greg か他の読者が教えてくれるはずです。私は手のひらに顔を向ける準備ができています!
もちろん、適切なバリアのコストが、インスタンスごとに1回の動作を取得するためinit
に使用することから得られるメリットに見合う価値があるdispatch_once()
かどうか、または別の方法で要件に対処する必要があるかどうかを判断する必要があります。そのような代替手段は、この回答の範囲外です!
のコードdispatch_atomic_maximally_synchronizing_barrier()
:
dispatch_atomic_maximally_synchronizing_barrier()
独自のコードで使用できる、Apple のソースから適用された の定義は次のとおりです。
#if defined(__x86_64__) || defined(__i386__)
#define dispatch_atomic_maximally_synchronizing_barrier() \
({ unsigned long _clbr; __asm__ __volatile__( "cpuid" : "=a" (_clbr) : "0" (0) : "ebx", "ecx", "edx", "cc", "memory"); })
#else
#define dispatch_atomic_maximally_synchronizing_barrier() \
({ __c11_atomic_thread_fence(dispatch_atomic_memory_order_seq_cst); })
#endif
これがどのように機能するかを知りたい場合は、Apple のソース コードを読んでください。