前文:truecryptコンテナを復元するプロジェクトに取り組んでいます。おそらくランダムな順序で300万を超える小さなファイルにカットされました。目標は、暗号化キーを含むコンテナーの先頭または末尾を見つけることです。
そのために、メインのマウントまたはバックアップヘッダーの復元を同時に試みる多くのtruecryptプロセスを開始する小さなrubyスクリプトを作成しました。truecryptとの相互作用は、生成されたPTYを介して発生します。
PTY.spawn(@cmd) do |stdout, stdin, pid|
@spawn = {stdout: stdout, stdin: stdin, pid: pid}
if test_type == :forward
process_truecrypt_forward
else
process_truecrypt_backward
end
stdin.puts
pty_expect('Incorrect password')
Process.kill('INT', pid)
stdin.close
stdout.close
Process.wait(pid)
end
これはすべて正常に機能し、テストコンテナの必要な部分を正常に見つけることができます。物事をスピードアップするために(そして私は300万個以上を処理する必要があります)、私は最初にRuby MRIマルチスレッドを使用し、問題について読んだ後、concurent-rubyに切り替えました。
私の実装は非常に簡単です。
log 'Starting DB test'
concurrent_db = Concurrent::Array.new(@db)
futures = []
progress_bar = initialize_progress_bar('Running DB test', concurrent_db.size)
MAXIMUM_FUTURES.times do
log "Started new future, total #{futures.size} futures"
futures << Concurrent::Future.execute do
my_piece = nil
run = 1
until concurrent_db.empty?
my_piece = concurrent_db.slice!(0, SLICE_PER_FUTURE)
break unless my_piece
log "Run #{run}, sliced #{my_piece.size} pieces, #{concurrent_db.size} left"
my_piece.each {|a| run_single_test(a)}
progress_bar.progress += my_piece.size
run += 1
end
log 'Future finished'
end
end
74個のCPUコアを備えた大規模なAWSインスタンスをレンタルして、「これで高速に処理する」と考えたよりも。しかし、問題は、同時に起動するフューチャー/スレッドの数(つまり、20または1000)に関係なく、1秒あたり最大50チェックに達しないことです。
1000スレッドを起動すると、CPU負荷は20〜30分間だけ100%に保たれ、その後、15%に達するまで低下し、そのままになります。そのような実行内の典型的なCPU負荷のグラフ。ディスクの負荷は問題ではありません。AmazonEBSストレージを使用して最大3MiB /秒に達しています。
何が足りないのですか?100%CPUを使用して、パフォーマンスを向上させることができないのはなぜですか?
マルチスレッドのメリットが見られない理由を正確に言うのは難しいです。しかし、これが私の推測です。
と呼ばれる実行に10秒かかる非常に集中的なRubyメソッドがあるとしましょうdo_work
。さらに悪いことに、このメソッドを100回実行する必要があります。1000秒待つのではなく、マルチスレッド化を試みることができます。これにより、CPUコア間で作業が分割され、ランタイムが半分になるか、4分の1になる可能性があります。
Array.new(100) { Thread.new { do_work } }.each(&:join)
しかし、いいえ、これはおそらくまだ1000秒で終了します。どうして?
この例を考えてみましょう。
thread1 = Thread.new { class Foo; end; Foo.new }
thread2 = Thread.new { class Foo; end; Foo.new }
Rubyでクラスを作成すると、内部で多くのことが行われます。たとえば、実際のクラスオブジェクトを作成し、そのオブジェクトのポインタをグローバル定数に(ある順序で)割り当てる必要があります。thread1がそのグローバル定数を登録し、実際のクラスオブジェクトの作成の途中で、thread2が実行を開始すると、「ああ、Foo
すでに存在します。先に進んで実行しましょう」と言いFoo.new
ます。クラスが完全に定義されていない場合はどうなりますか?または、thread1とthread2の両方が新しいクラスオブジェクトを作成し、両方がクラスをとして登録しようとした場合はFoo
どうなりますか?どちらが勝ちますか?作成され、現在登録されていないクラスオブジェクトはどうですか?
これに対する公式のRubyソリューションは単純です。実際にこのコードを並行して実行しないでください。代わりに、「グローバルVMロック」と呼ばれる単一の大規模なミューテックスがあり、Ruby VMの状態を変更するもの(クラスの作成など)を保護します。したがって、上記の2つのスレッドはさまざまな方法でインターリーブされる可能性がありますが、各VM操作は基本的にアトミックであるため、VMが無効な状態になることはありません。
これは私のラップトップで実行するのに約6秒かかります:
def do_work
Array.new(100000000) { |i| i * i }
end
これには約18秒かかります明らかに
3.times { do_work }
ただし、GVLはスレッドが実際に並行して実行されるのを防ぐため、これにも約18がかかります。
Array.new(3) { Thread.new { do_work } }.each(&:join)
これも実行に6秒かかります
def do_work2
sleep 6
end
しかし、これも実行に約6秒かかります。
Array.new(3) { Thread.new { do_work2 } }.each(&:join)
どうして?Rubyのソースコードを掘り下げるsleep
と、最終的にC関数が呼び出さnative_sleep
れ、そこに次のように表示されます。
GVL_UNLOCK_BEGIN(th);
{
//...
}
GVL_UNLOCK_END(th);
Ruby開発者はsleep
、これがVMの状態に影響を与えないことを知っているため、GVLのロックを明示的に解除して、GVLを並行して実行できるようにしました。GVLをロック/ロック解除するものと、GVLのパフォーマンス上の利点をいつ確認するかを正確に把握するのは難しい場合があります。
私の推測では、コード内の何かがGVLにヒットしているため、スレッドの一部が並行して実行されている間(通常、サブプロセス/ PTYのものはすべて実行されます)、Ruby VM内でそれらの間で競合が発生し、一部がシリアル化されます。
真に並列なRubyコードを取得するための最善の策は、次のように単純化することです。
Array.new(x) { Thread.new { do_work } }
これdo_work
は、サブプロセスの生成など、GVLのロックを確実に解除する単純なものであると確信している場合です。Truecryptコードを小さなシェルスクリプトに移動してみてください。そうすれば、Rubyが実行された後、Rubyがコードと対話する必要がなくなります。
いくつかのサブプロセスを開始するだけの小さなベンチマークから始めて、それらをシリアルに実行する時間を比較することによって、それらが実際に並列に実行されていることを確認することをお勧めします。
この記事はインターネットから収集されたものであり、転載の際にはソースを示してください。
侵害の場合は、連絡してください[email protected]
コメントを追加