キキキキキキキキキキキキキキキキキキキキキキ キキ #DSIRLNP @kumagi
キャッシュコヒーレントに囚われない並列カウンタ達
#DSIRLNP@kumagi
この発表について
DSIRData Structure!
キャッシュ?• CPU が高速化のためにメモリの一部を切り
出して複製している物
キャッシュCPU コア
L1 キャッシュ
L2 キャッシュ
L3 キャッシュ
メモリ
All programmer should know
• L1 キャッシュ 参照 ......................... 0.5 ns• 分岐予測ミス ............................ 5 ns• L2 キャッシュ 参照 ........................... 7 ns• Mutex lock/unlock ........................... 25 ns• Main memory 参照 ...................... 100 ns
キャッシュ
メモリ
キャッシュコヒーレント?• 複数の CPU コアから見えるメモリは同一
でないと困る• つまり複数の CPU コアのキャッシュは常
に最新の情報を保持してないと困る• だが常に最新の情報を全部のキャッシュ
に全て書き続けるのは速度が出ないので、まるで本当に全部のキャッシュにすべて書いてるかのように見せかけながらパフォーマンスを出す必要がある
キャッシュ
メモリ
2 つのコアでL2 キャッシュを共有
キャッシュコヒーレント• 複数の L1 キャッシュ間で一貫したデータを扱
うためのキャッシュ間のプロトコル• Intel 系 CPU は MESIF プロトコル• AMD 系 CPU は MOESI プロトコル
Core1 Core2
L1 キャッシュ L1 キャッシュ
L2 キャッシュ
コヒーレント
キャッシュコヒーレントプロトコル
• キャッシュラインが取る状態名の頭文字が由来– Modified: メモリよりもキャッシュの方が新しい ( 書き換
えた )– Exclusive: メモリとキャッシュが同一であり、他のコアは
このキャッシュラインを持っていない– Shared: メモリとキャッシュが同一であり、他のコアも
このキャッシュラインを複製している– Invalid: 正しいキャッシュを持っていないので読むな– Owned: 俺がメモリだ (Shared 可能な Modified)– Forward: Shared のボス。アクセスする際にはこいつに伺
え
L1 キャッシュの状態• 1 ライン 64byte で、アドレスの下位バイ
トが同じ物ごとに 8way ずつ 32KB まで持っている
• 1 ラインごとに MESIF のどれかのステートを持っているinvalid
shared
exclusive
modified
64Byte
32KB
全部のラインが独立してそれぞれステートが遷移する
forward
共有した
キャッシュコヒーレントプロトコル
• MESI(F) プロトコルは Modified なデータを他のコアが読み出す際にメモリに書き戻す
• Shared なデータを書き換える時には Invalidation 要求をブロードキャストするためトラフィックが混む
Modified
Shared Invalid
Exclusive
共有を要求( 1 個下のメモリへアクセス)
Invalidation要求が来た
新規に読み出した
書き換えた
他のコアから貰った
キャッシュを汚す事はギルティ• 他のスレッドから繰り返し読む値を書き換え続けると、
キャッシュラインのステートは Modified と Shared の間を行ったりきたりすることになる。これは激しいトラフィックを起こす。
• 更には Modified から Shared になる度に下層のメモリに書き込まなくてはならない
Core1 Core2
L1 キャッシュ L1 キャッシュ
L2 キャッシュ
write
read
うぉー!
うぉー!
ぎゃー!!write
Invalidate
キャッシュを汚す事はギルティ
http://www.1024cores.net/home/lock-free-algorithms/first-things-first から
共有データにWrite した場合の速度
local データにWrite した場合の速度
共有 /local データから Read した場合の速度
http://www.1024cores.net/home/lock-free-algorithms/first-things-first から
まったくスケールしない!!!!
NUMA でのキャッシュコヒーレント
• 最近のサーバはマルチソケット CPU がよく使われる
cc-NUMA
• 複数の CPU ソケットから同一のメモリ空間が見えて欲しい(まるでマルチコア CPUのように)
• キャッシュ衝突すると QPI アクセスの刑– かつては拷問にも使われていた QPI 経由の
キャッシュコヒーレント
All programmer should know
• L1 キャッシュ 参照 ......................... 0.5 ns• 分岐予測ミス ............................ 5 ns• L2 キャッシュ 参照 ........................... 7 ns• Mutex lock/unlock ........................... 25 ns• Main memory 参照 ...................... 100 ns• QPI 経由で隣のメモリ参照 .............. 200 ns
~
キャッシュ
メモリ メモリ
隣の CPU のキャッシュは自分のメインメモリよりも遠い!!!
cc-NUMA の時代• キャッシュ衝突をとにかく減らすアルゴ
リズムが望まれている
Combining Tree
• 以下のインタフェースを備えるカウンタ– add(int a) :数値 a を足す– read() :現在の数値を読む
• ただしスケーラブル!class counter {public: counter() : cnt_(0) {} void add(int a) { cnt_ += a; } int read() const { return cnt_; }private: int cnt_;};
擬似コード
Combining Tree
• 1 つのキャッシュラインを取り合うのが 2スレッドまでになるようトーナメントを構成する
• トーナメントでぶつかったスレッドは、先に来たスレッドに後に来たスレッドが値を託して待つ
• キャッシュコヒーレントトラフィックを劇的に削減!
Combining Tree
• トーナメントのてっぺんを読めば値が読める
root
Combining Tree アルゴリズム
+1 +1 +2 +3 +2 +1
Combining Tree アルゴリズム
+1 +1 +2 +3 +2 +1
Combining Tree アルゴリズム
+1
待
+3
+3
待
+3
Combining Tree アルゴリズム
+4
待
待
+6
待
待
Combining Tree アルゴリズム
+10
待
待
待
待
待
Combining Tree アルゴリズム• 結合法則を用いて計算の合成を行う• x + 1 + 1 + 2 + 3 + 2 + 1• x + 1 + (1 + 2) + 3 + (2 + 1)• x + (1 + 3) + (3 + 3)• x + (4 + 6)• x + 10
詳細なアルゴリズム• 各ノードは Idle, First, Second, Root のどれ
かの状態を持つ– Idle: どのスレッドも触ってない– First: 最初のスレッドが既に触った– Second: 二つ目のスレッドが触った– Root: てっぺん ( 遷移しない )
• 更にノードはロックを 2 つ持っている
Combining Tree• 初めに木を登りながらステートの変更を
行う– Idle な物を First にしていく– ロックを取ってステートを変えて即アンロッ
クRoot
First
First
Combining Tree• 初めに木を登りながらステートの決定を
行う– First を見たら Second にして第二ロックを獲得• こっちのロックはすぐには手放さないRoot
First
First First
Second
Second
Combining Tree• 初めに木を登りながらステートの決定を
行う– Second にしたらそこで木を登るの停止
Root
First
First
Second
Second
Combining Tree• 木を登るのをやめた所で、自分の過去の
経路の第二ロックを獲得し直しながら値を清算
Root
First
First
Second
Second
Combining Tree• 木を登るのをやめた所で、自分の過去の
経路の第二ロックを獲得し直しながら値を清算
Root
First
First
Second
Second+2+1
Combining Tree• 清算しきった値を最初に自分が登った一番高い所に書き込んでアンロックするのが基本戦略
Root
First
First
Second
Second
+3Second
Combining Tree• 清算しきった値を最初に自分が登った一番高い所に書き込んでアンロック
Root
FirstSecond
Second
Second
FirstFirst
+3
Combining Tree• Second のステートを見たら他のスレッド
が清算中なのでそれを待つ(第二ロック獲得待ち)
Root
FirstSecond
Second
Second
FirstFirstLock!
Lock!
Combining Tree• 清算し終わった値を持って再帰的に自分
の値として生産する
Root
FirstSecond
Second
Second
FirstFirst
+4
Combining Tree
• 平均して木の深さ n に対して 2*n回のロックとアンロックを使うことになる–大丈夫なの・・・?
• Mutex lock/unlock ........................... 25 ns• QPI 経由で隣のメモリ参照 .............. 200 ns
~
キャッシュ衝突のペナルティを考えると余裕でペイする
Combining Tree
• 衝突がない場合でも 2*n回のロック・アンロック
• レイテンシという点において非常に悪い–改良として 1 ノードに 3 スレッド以上ぶら下げて木の深さを減らすパターンもあるが、複雑さがマッハでレイテンシはむしろ悪化
Combining Funnel
• Funnel = 上戸• 乱数ベースで負荷を低減– アルゴリズムは Elimination に近い– Elimination と組み合わせることも可能
Counter
Combining Funnel
• 配列の中のランダムな箇所に CAS命令で自分のタスクを置く
• ランダム時間の待機後、 CAS でタスクを引き上げて次のレイヤーに進む ( ここが上戸っぽい)
+2 +1
Combining Funnel
• 置こうと思った場所に先客が居たらそいつと操作を結合して次のレイヤーに進む
+2 +1
+3 したい
Combining Funnel
• 置こうと思った場所に先客が居たらそいつと操作を結合して次のレイヤーに進む
待 +1
+5 せざるを得ない!
Combining Funnel
• 置こうと思った場所に先客が居たらそいつと操作を結合して次のレイヤーに進む
待 +1
+5
Counter
あっ待てって言われてる
Combining Funnel
• カウンタのレイヤーまで到達したらそいつに Compare And Swap
• 感動的に簡単しかも速い
Combining Funnel
Combining Funnels A Dynamic Approach To Software Combining より
SNZI
• 数を数えようとするから残念なことになるんや!諦めろ!–ゼロかどうか分かればええ!
• Scalable-Non-Zero-Indicator の略で SNZI– pronounced "snazzy" : (和訳 )粋な、おしゃれ
な、優雅な、エレガントな、格好いい
SNZI のセマンティクス• 数字が読めないカウンタ• これをスケーラブルにする
class snzi {public: counter() : cnt_(0) {} void inc() { cnt_ += 1; } void dec() { cnt_ -= 1; } bool is_zero() const { return cnt_ == 0; }private: int cnt_;};
擬似コード
SNZI の実装• キャッシュラインで木構造を作る
0
0 0
0 0 0 0
ここが 0 かどうかを
見れば良い
SNZI の実装• 木の各ノードが 1CPU–上から数えて i番目のノードは i番目の CPU に割当
0
0 0
0 0 0 01
1
1
root 以外を 0 から 1 に増やすときには 1 個上のノードの値をインクリメントするroot は常に普通にインクリメントするだけ
SNZI の実装• 木の各ノードが 1CPU–上から数えて i番目のノードは i番目の CPU に割当
0
0 0
0 0 0 01
1
1
root 以外を 1 から 0 に減らすときには 1 個上のノードの値をデクリメントするroot は常に普通にデクリメントするだけ
SNZI の実装• 木の各ノードが 1CPU–上から数えて i番目のノードは i番目の CPU に割当
0
0 0
0 0 0 01
1
1
それ以外のときは普通にインクリメントするだけ=上のノードのキャッシュラインに触れずに済む
2
SNZI• すごくスケールする!!!
SNZI: Scalable NonZero Indicators より
しかもロックフリー!!
SNZI の実装• 厳密には複数のスレッドが同一のノード
にアクセスしに来た際に 0 1⇔ 周りで細かい調停が必要
0 から 1 のときは、先に自分の箇所の値を 1/2 という値にしてから上のノードをインクリメントしにいき、そのあとで 1/2 を 1 に CAS を試みる。もし上のノードのインクリメントに成功した後に 1/2→1 の CAS に失敗したら上のノードをデクリメントしておく• デクリメントの数はその前に行われたイン
クリメントの数を越えては行けない( Read-Write ロックなどに用いるには充分)
SkySTM への適用
What kinds of applications can benefit from Transactional Memory? より
時間が無くて書けなかったこと• STM の高速化のために SNZI が必要とか
いっときながら実際に SNZI を Read-Write-Lock に用いた SkySTM は割とあっさりTLRW に負けている– TLRW は ByteLock っていうまた別のロックプ
ロトコルを用いてる(こっちの話はまた今度)
• SNZI のために HTM を用いたやつがあって凄い速い– HTM の使い方についてもまた今度
HTM+SNZI
HTMの力
What kinds of applications can benefit from Transactional Memory? より