Python 2.6 multiprocessing package を触ってみた。 [GIL回避]
oktです。
普段、並列処理のプログラムを書くことは滅多にないんですが、今後のために GIL(Global Interpreter Lock) による影響を軽く確認しておこうと思ったのと、Python2.6 で実装されたmultiprocessing パッケージに触れておこうという事でネタにしてみました。
PythonやRubyでマルチスレッドな並列処理を書こうと思ったらGILの問題にぶち当たります。
最近のサーバでは2コア4コアが当たり前なのですが、GILの問題があると複数のCPUを有効利用できません。それを以下の言語とサンプルコードを使って状況確認します。
サンプルコードは、0から100,000,000までインクリメントするスレッドを4つ作成し、すべてのスレッドの完了を待って終了するものです。
動作環境はCentOS5(VMware Server上で動作)で仮想CPUを2個に設定しています。
使用する言語は下記の5つ。
- Python 2.4.3
- Python 2.6.1
- Python 3.0
- Ruby 1.8.5
- Ruby 1.9.1-p0
Python2.4.3/2.6.1用threadingパッケージ利用時のサンプルコード(test.py)
from threading import * class TestThread(Thread): def run(self): i = 0 while i < 100000000: i = i + 1 if __name__ == "__main__": mainthread = currentThread() for i in range(4): thread = TestThread() thread.start() for t in enumerate(): if mainthread != t: t.join()
Python3.0用threadingパッケージ利用時のサンプルコード(test-3.py)
from threading import * class TestThread(Thread): def run(self): i = 0 while i < 100000000: i = i + 1 if __name__ == "__main__": mainthread = current_thread() for i in range(4): thread = TestThread() thread.start() for t in enumerate(): if mainthread != t: t.join()
Ruby1.8.5/1.9.1-p0用サンプルコード(test.rb)
for num in 1..4 Thread.start(0) do |s| n = s while n < 100000000 n = n + 1 end end end Thread.list.each do |s| if Thread.current != s s.join end end
Python2.4.3/Python 2.6.1/Python 3.0で実行した時のtopコマンドの表示は下記のようになりました。
top - 16:20:12 up 17:47, 4 users, load average: 0.31, 0.07, 0.02
Tasks: 55 total, 1 running, 54 sleeping, 0 stopped, 0 zombie
Cpu0 : 2.0%us, 43.0%sy, 0.0%ni, 55.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu1 : 15.2%us, 62.6%sy, 0.0%ni, 22.2%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 515444k total, 500240k used, 15204k free, 64172k buffers
Swap: 1048568k total, 4k used, 1048564k free, 369836k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
28398 okt 17 0 27408 2464 1460 S 123 0.5 0:07.09 /usr/bin/python ./test.py
3バージョンともCPUの利用傾向に差は見られません。
system のCPU利用率が高いのはfutex(2)でのスレッド制御にCPUを消費しているからのようです。
Ruby1.8.5/Ruby1.9.1-p0で実行した時のtopコマンドの表示は下記のようになりました。
top - 16:22:13 up 17:49, 4 users, load average: 0.31, 0.07, 0.02
Tasks: 55 total, 3 running, 52 sleeping, 0 stopped, 0 zombie
Cpu0 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu1 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 515444k total, 499620k used, 15824k free, 64148k buffers
Swap: 1048568k total, 4k used, 1048564k free, 369836k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
28395 okt 25 0 13776 1856 1088 R 101 0.4 0:07.98 /usr/bin/ruby ./test.rb
こちらはPythonと違い、userのCPU使用率が100%になっています。
Ruby1.9.1-p0は configure時に '—-enable-pthread’ をつけて作成したのですが
Pythonほど過剰にfutex(2)をコールしていないようで、systemのCPU使用率はかなり低くなっています。
こんな具合でPythonとRubyで違いはあれど、複数のCPUを効率的に使えていないのが分かります。
メモリの利用効率等を別にしてCPUでの処理効率を重要視するなら、マルチプロセスで並列処理をするか、GIL的な機構を採用していない言語を使う必要があるようです。
ちなみに、上記の5つのケースでtimeコマンドを使用して実行時間をみるとこんな感じになりました。
Python 2.4.3(threading利用):
$ /usr/bin/python -V
Python 2.4.3
$ time /usr/bin/python test.py
real 1m16.848s
user 0m13.027s
sys 1m40.013s
Python 2.6.1(threading利用):
$ /usr/local/bin/python -V
Python 2.6.1
$ time /usr/local/bin/python test.py
real 1m4.873s
user 0m10.973s
sys 1m20.533s
Python 3.0(threading利用):
$ /usr/local/python3000/bin/python -V
Python 3.0
$ time /usr/local/python3000/bin/python test-3.py
real 1m26.549s
user 0m29.197s
sys 1m29.717s
Ruby 1.8.5:
$ /usr/bin/ruby -v
ruby 1.8.5 (2006-08-25) [i386-linux]
$ time /usr/bin/ruby test.rb
real 1m30.336s
user 1m30.301s
sys 0m0.028s
Ruby 1.9.1-p0( –enable-pthread):
$ /usr/local/ruby191p0/bin/ruby -v
ruby 1.9.1p0 (2009-01-30 revision 21907) [i686-linux]
$ time /usr/local/ruby191p0/bin/ruby test.rb
real 0m11.447s
user 0m11.367s
sys 0m0.040s
Ruby 1.9.1-p0 が圧倒的に実行時間が短いですね。
で、ここからが本題です(前置き長すぎ)。
先ほどのPythonのthreadingパッケージを使ったマルチスレッド処理を、Python 2.6から実装されたmultiprocessingパッケージを利用したものに改変してみましょう。
Python multiprocessing 利用のサンプルコード(test-mp.py)
from multiprocessing import * class TestThread(Process): def run(self): i = 0 while i < 100000000: i = i + 1 if __name__ == "__main__": for i in range(4): thread = TestThread() thread.start() for t in active_children(): t.join()
Python2.x threadingパッケージ利用時のサンプルコード(test.py)との差異はほとんどありません。
何点か置換するだけで済みました。
上記のサンプルコードをPython2.6.1で実行した時のtopコマンドの表示は下記のようになりました。
top - 16:23:23 up 17:50, 4 users, load average: 0.31, 0.07, 0.02
Tasks: 57 total, 3 running, 54 sleeping, 0 stopped, 0 zombie
Cpu0 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu1 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 515444k total, 496512k used, 18932k free, 42392k buffers
Swap: 1048568k total, 4k used, 1048564k free, 394228k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
5740 okt 25 0 8148 2264 424 R 50 0.4 0:01.13 /usr/local/bin/python test-mp.py
5741 okt 25 0 8148 2264 424 R 50 0.4 0:01.12 /usr/local/bin/python test-mp.py
5742 okt 25 0 8148 2264 424 R 50 0.4 0:01.13 /usr/local/bin/python test-mp.py
5743 okt 25 0 8148 2264 424 R 50 0.4 0:01.12 /usr/local/bin/python test-mp.py
5739 okt 20 0 8148 3540 1700 S 0 0.7 0:00.03 /usr/local/bin/python test-mp.py
当たり前ですが、Cpu0,Cpu1の両方が100%フルに使われているのが分かります。
ちなみに、timeコマンドを使用して実行時間をみると下記のようになりました。
Python 2.6.1(multiprocessing利用):
$ /usr/local/bin/python -V
Python 2.6.1
$ time /usr/local/bin/python test-mp.py
real 0m12.125s
user 0m23.841s
sys 0m0.120s
単純に、Python 2.6.1でthreadingを利用したケースと比較すると大幅に高速化されました。
すべてのパターンに当てはまるわけではないですが、処理内容によっては、
リソースに余裕があるのであればmultiprocessingを使った方が高速に処理できそうです。
multiprocessingの利点は他にもあるのですが、それはまたの機会に。
ディスカッション
コメント一覧
Rubyを使い始めて2年になります。
この記事は、かなり面白いです。
Ruby1.9を使おうかなと思っているのですが、
まだ発展途上みたいですので、Ruby2.0が待ちどうしいです。