Share
Embed
Transcript
37 slides
-
What’s GVL What’s GVL • Global(or Giant) VM Lock •
あらゆる処理を単一のロックで保護 • 実装が簡単。multi thread issueの多くをRuby scriptから隠 蔽できる。などの利点 • 一方システムが複数のCPUを持つときにスケールしない 元凶でもある • Pythonも同様のしくみ • Global(or Giant) VM Lock • あらゆる処理を単一のロックで保護 • 実装が簡単。multi thread issueの多くをRuby scriptから隠 蔽できる。などの利点 • 一方システムが複数のCPUを持つときにスケールしない 元凶でもある • Pythonも同様のしくみ -
What’s GVL (cont.) What’s GVL (cont.) •GVLを持っているスレッドだけが走行できる •よって、GVL実装がマルチスレッドプログラ ムの挙動を決める •OSのスケジューラと悩みが似ていて面白い
•GVLを持っているスレッドだけが走行できる •よって、GVL実装がマルチスレッドプログラ ムの挙動を決める •OSのスケジューラと悩みが似ていて面白い -
モチベーション モチベーション f = false Thread.new { ....; f =
true } Thread.pass until f f = false Thread.new { ....; f = true } Thread.pass until f •1.9.2でハングする(かもしれ ない)スクリプト例 •特にLinux •従来の実装は最近のCPUと相性 がよくなかった •1.9.2でハングする(かもしれ ない)スクリプト例 •特にLinux •従来の実装は最近のCPUと相性 がよくなかった -
1.9.2のGVL design 1.9.2のGVL design • Lockといいつつ、基本的にはずっと保持しっぱなし。 waitする可能性のあるシステムコール(e.g. write)を呼ぶと きだけ離す •
lock, unlockはコストが高い操作なので出来る限り回数を 減らしたい。メソッド呼び出しのたびにロック取得開放 なんてバカバカしい。Python, QEMU, 昔のLinuxも似たよ うなもの • GVL acquire/releaseの実態はただの pthread_mutex_{lock,unlock} • Lockといいつつ、基本的にはずっと保持しっぱなし。 waitする可能性のあるシステムコール(e.g. write)を呼ぶと きだけ離す • lock, unlockはコストが高い操作なので出来る限り回数を 減らしたい。メソッド呼び出しのたびにロック取得開放 なんてバカバカしい。Python, QEMU, 昔のLinuxも似たよ うなもの • GVL acquire/releaseの実態はただの pthread_mutex_{lock,unlock} -
1.9.2のGVL design (cont.) 1.9.2のGVL design (cont.) •それだけではビジーループ時にまったくスレ ッドスイッチしなくなって悲しい •そこでタイマースレッド。10msに一回 「GVL
を開放してください」フラグをONにしてた •それだけではビジーループ時にまったくスレ ッドスイッチしなくなって悲しい •そこでタイマースレッド。10msに一回 「GVL を開放してください」フラグをONにしてた -
1.9.2のGVL design (cont.) 1.9.2のGVL design (cont.) if (th->interrupt_flag) { gvl_release();
sched_yield(); gvl_acquire(); } if (th->interrupt_flag) { gvl_release(); sched_yield(); gvl_acquire(); } •フラグがONされると右の 擬似コードのような処理 を行っていた •フラグがONされると右の 擬似コードのような処理 を行っていた -
動かないコード例 動かないコード例 •右は別のOSSで実際にトラ ブル(ハング)に発展したコ ードの抜粋 •なぜ? •thread1が常にlockを取っ てしまうのでhoge=1は実 行されない •右は別のOSSで実際にトラ
ブル(ハング)に発展したコ ードの抜粋 •なぜ? •thread1が常にlockを取っ てしまうのでhoge=1は実 行されない while (true) { lock(); if (hoge) { unlock(); break; } unlock(); rep_nop(); } lock(); hoge = 1; unlock(); thread1 thread2 -
最近のCPU 最近のCPU Memory1にアクセスするのはCPU1の方が速い だって近いもん よってrace発生時の勝率は五分五分ではない memory 1 cor e cor
e cor e cor e cpu1 cor e cor e cor e cor e cpu2 memory 2 おせー -
if (th->interrupt_flag) { gvl_release(); sched_yield(); gvl_acquire(); } if (th->interrupt_flag) {
gvl_release(); sched_yield(); gvl_acquire(); } •gvl_release()とgvl_acquire()の間にsched_yield()あ るじゃん。こいつは何をするものなんだぜ? •なぜsched_yield()は、ちゃんとyieldしてくれない のか ちょっと戻って ちょっと戻って -
sched_yield sched_yield •Unix でもっとも誤解されてるAPIのひとつ •超訳:スレッドの実行順を管理するリストの先頭にくるまで プロセッサを手放すよん •Unix でもっとも誤解されてるAPIのひとつ •超訳:スレッドの実行順を管理するリストの先頭にくるまで プロセッサを手放すよん
http://pubs.opengroup.org/onlinepubs/009695399/functions/sc hed_yield.html int sched_yield(void); • The sched_yield() function shall force the running thread to relinquish the processor until it again becomes the head of its thread list. -
昔のスケジューラ 昔のスケジューラ cor e Thread Thread Thread たった1つの 実行待ちキュー Thread
cor e Thread cor e Thread -
現代のス(ry 現代のス(ry cor e Thread Thread cor e Thread cor
e Thread Thread Thread CPUの数だけ実行待ちキ ュー Thread -
ありがちなこと ありがちなこと cor e Thread Ruby Thread1 cor e Thread
Ruby Thread2 Yield! -
ありがちなこと ありがちなこと cor e Thread Ruby Thread1 cor e Thread
Ruby Thread2 おいら結局順番 まわってこなか った。ショボー ン thread listのheadに きたぞー -
ありがちなこと ありがちなこと cor e Thread Ruby Thread1 cor e Thread
Ruby Thread2 POSIX「くくく、譲る、譲るとはいったがお前に譲る と約束した覚えはない!」 -
CFS yield問題 CFS yield問題 •LinuxではRHEL5世代とRHEL6世代でスケジューラが違う •RHEL6世代のスケジューラ(CFS)はsched_yieldを無視する (RHEL5と同等動作にするknobはある) •なぜってJavaがクレイジーだから •Linux upstreamでは上記knobも消えてるので、結局1CPU
でもなんらかの対策が必要 •LinuxではRHEL5世代とRHEL6世代でスケジューラが違う •RHEL6世代のスケジューラ(CFS)はsched_yieldを無視する (RHEL5と同等動作にするknobはある) •なぜってJavaがクレイジーだから •Linux upstreamでは上記knobも消えてるので、結局1CPU でもなんらかの対策が必要 -
アイデア アイデア gvl_acquire mutex_lock(&lock->lock) cond_wait(&lock->wait) lock->acquired = 1; // ロックとったよと通知
cond_signal(&lock->switch_cond) mutex_unlock(&lock->lock) mutex_lock(&lock->lock) lock->acquired = 0; cond_signal(&lock->wait_cond) // ロック取るまで寝る cond_wait(&lock->switch_cond) mutex_unlock(&lock->lock) gvl_release •ならば無理やりfairnessにしてやろう •同一スレッドが二回連続GVLを取れなくしてみる •ならば無理やりfairnessにしてやろう •同一スレッドが二回連続GVLを取れなくしてみる -
結果 結果 1byteずつ書くスレッドと1byte ずつ読むスレッド。 小IO多発 = GVL switch多発 before: 1.26
after: 2.49 1byteずつ書くスレッドと1byte ずつ読むスレッド。 小IO多発 = GVL switch多発 before: 1.26 after: 2.49 lmax = 100_000 r, w = IO.pipe [Thread.new{ lmax.times{ w.write('a') } p "w:exit" }, Thread.new{ lmax.times{ r.read(1) } p "r:exit" vm_thread_pipe -
1.9.3のデザイン 1.9.3のデザイン •1.9.3ではIO時のGVL releaseと、タイマースレッド経由の GVL releaseを分けた •IO等からの自発的GVLは1.9.2と同じ。小IOではGVLが switchしないこともある •タイマースレッド経由、つまりスレッドが長時間走行し すぎと判定された場合のGVL
releaseは強制switchあり。絶 対自分がもう一度GVLを取らないよう寝る •手抜きだけどだいたい動く •1.9.3ではIO時のGVL releaseと、タイマースレッド経由の GVL releaseを分けた •IO等からの自発的GVLは1.9.2と同じ。小IOではGVLが switchしないこともある •タイマースレッド経由、つまりスレッドが長時間走行し すぎと判定された場合のGVL releaseは強制switchあり。絶 対自分がもう一度GVLを取らないよう寝る •手抜きだけどだいたい動く -
性能 性能 0 0.5 1 1.5 2 2.5 create_join mutex2
pass_flood 1.9.2 1.9.3 -
考察 考察 •だいたい1.9.2よりも悪化する •コードが増えると遅くなる。の法則 •ワーストケースで二倍おそい そんなスクリプト書くな、というコミッタか らのメッセージ(ぉ •ベストケースは •だいたい1.9.2よりも悪化する •コードが増えると遅くなる。の法則
•ワーストケースで二倍おそい そんなスクリプト書くな、というコミッタか らのメッセージ(ぉ •ベストケースは -
1.9.2: 3245.6秒 (~ 1H) 1.9.3: 4.4秒 1.9.2: 3245.6秒 (~ 1H)
1.9.3: 4.4秒 vm_thread_mutex3 vm_thread_mutex3 -
今日おぼえて帰ってほしいこと 今日おぼえて帰ってほしいこと • 現代的なCPUはメモリアクセス速度が均一じゃない • GVLとイマドキのCPUは大変相性が悪い • みなさんが気持よくRubyが書けるように中の人は一生懸 命魔法をかけてます •
現代的なCPUはメモリアクセス速度が均一じゃない • GVLとイマドキのCPUは大変相性が悪い • みなさんが気持よくRubyが書けるように中の人は一生懸 命魔法をかけてます