blog

マルチスレッドとマルチプロセッサーによるPython並行プログラミング

Pythonコーディングでよく議論されるのは、シミュレーション実行のパフォーマンスを最適化する方法です。定量的なコードを考える場合、NumPy、SciPy、pandasはすでにこの点で非常に有用ですが...

Oct 5, 2014 · 9 min. read
シェア

Pythonコーディングでよく議論されるのは、シミュレーション実行のパフォーマンスを最適化する方法です。定量的なコードを考える際には、NumPy、SciPy、pandasはすでにこの点で非常に有用ですが、システムを構築する際には、これらのツールを効果的に使うことはできません。コードを高速化する方法は他にもあるのでしょうか?答えはイエスですが、注視すべきことです!

この記事では、Pythonプログラムに導入できる別のモデルを見てみましょう。モンテカルロ・シミュレータは、アルゴリズム取引のテストだけでなく、オプション価格のようなタイプの様々なパラメータのシミュレーションを行うために使用できます。

Python

マルチコアマシンでは、マルチスレッドであることを期待するコードは余分なコアを使用し、全体的なパフォーマンスを向上させます。残念ながら、メインの Python インタプリタは内部的にはマルチスレッドではありません。

GILが必要なのは、Pythonインタプリタが非スレッドセーフだからです。つまり、スレッド内から Python オブジェクトに安全にアクセスしようとすると、グローバルにロックがかかります。Python 命令の 100 バイトごとにインタプリタがロックを再取得し、I/0 操作をブロックします。ロックのせいで、CPU負荷の高いコードがスレッド化されたライブラリを使っても性能は上がりませんが、マルチプロセッシングライブラリを使うと性能は上がります。

並列ライブラリの実装

さて、上記の2つのライブラリを使用して、「小さな」問題に対する並行最適化を実装します。

スレッドライブラリー

前述のように、CPythonインタプリタを実行するPythonはマルチスレッドによるマルチコア処理をサポートしません。しかし、Pythonにはスレッドライブラリーあります。では、マルチコアを処理に使えないのであれば、このライブラリを使うことでどのようなメリットが得られるのでしょうか?

多くのプログラム、特にネットワーク通信やデータの入出力に関連するプログラムは、ネットワークの性能や入出力の性能によって制限されることがよくあります。Python インタプリタは、ネットワークアドレスやハードドライブなどの「リモート」ソースからデータを読み書きする関数呼び出しの返事を待ちます。その結果、そのようなデータアクセスはローカルメモリや CPU バッファからの読み込みよりもずっと遅くなります。

したがって、この方法で多くのデータソースにアクセスする場合、このデータアクセスのパフォーマンスを向上させる1つの方法は、アクセスする必要があるデータ項目ごとにスレッドを生成することです。

例えば、多くのサイトからURLをスリ取るためのPythonコードがあるとします。さらに、各URLをダウンロードするのに必要な時間が、コンピュータのCPUが処理できる時間よりもはるかに長いと仮定すると、これを実装するために1つのスレッドだけを使用することは、入出力のパフォーマンスによって大きく制限されることになります。

ダウンロードされるリソースごとに新しいスレッドを生成することで、このコードは複数のデータソースを並行してダウンロードし、すべてのダウンロードが終了した時点で結果を結合します。これは、後続の各ダウンロードが前のページのダウンロード終了を待たないことを意味します。この時点で、このコードは受信したクライアント/サーバーの帯域幅によって制限されます。

しかし、金融関連のアプリケーションの多くは、数値の処理が高度に集中化されているため、CPUの性能によって制限されています。そのようなアプリケーションでは、大規模な線形代数計算や、モンテカルロシミュレーション統計の実行など、値に対する確率統計が実行されます。そのため、そのようなアプリケーションでPythonとグローバルインタープリタロックを使用する限り、現時点ではPythonスレッドライブラリを使用しても性能は向上しません。

Pythonの実装

次の「おもちゃの」コードは、リストに数字を順次追加していくもので、マルチスレッド実装の一例です。各スレッドは新しいリストを作成し、乱数をリストに追加します。この選択された「おもちゃ」の例は非常にCPU集約的です。

以下のコードは、スレッディング・ライブラリへのインターフェースの概要を示したものですが、シングルスレッドで実装した場合よりも速くなるわけではありません。以下のコードにマルチプロセッシング・ライブラリを使用すると、総実行時間が大幅に短縮されることがわかります。

このコードがどのように動くか見てみましょう。まず、スレッディング・ライブラリをインポートします。最初の引数countは作成するリストのサイズを定義します。2番目の引数idは "job "のIDで、3番目の引数out_listは追加する乱数のリストです。

main__関数は107のサイズを作成し、2つのスレッドで作業を実行します。次に、切り離されたスレッドを保存するためにジョブのリストが作成されます。threading.Threadオブジェクトはlist_append関数を引数として受け取り、ジョブのリストに追加します。

最後に、ジョブは個別に開始され、「結合」されます。join() メソッドは、呼び出し元のスレッドを終了するまでブロックします。すべてのスレッドが実行を終了したことを確認する完全なメッセージがコンソールに出力されます。

# thread_test.pyimport randomimport threadingdef list_append(count, id, out_list):  
    """  
    Creates an empty list and then appends a   
    random number to the list 'count' number  
    of times. A CPU-heavy operation!  
    """ 
    for i in range(count):  
        out_list.append(random.random())if __name__ == "__main__":  
    size = 10000000   # Number of random numbers to add  
    threads = 2   # Number of threads to create  
 
    # Create a list of jobs and then iterate through  
    # the number of threads appending each thread to  
    # the job list   
    jobs = []  
    for i in range(0, threads):  
        out_list = list()  
        thread = threading.Thread(target=list_append(size, i, out_list))  
        jobs.append(thread)  
 
    # Start the threads (i.e. calculate the random number lists)  
    for j in jobs:  
        j.start()  
 
    # Ensure all of the threads have finished  
    for j in jobs:  
        j.join()  
 
    print "List processing complete." 

このコードは、コンソールから以下のコマンドで呼び出すことができます。

time python thread_test.py 

次のような出力が得られます。

List processing complete.  
real    0m2.003s 
user    0m1.838s 
sys     0m0.161s 

ユーザータイムとシステムタイムを足すと、実時間とほぼ等しくなることに注意してください。このことは、スレッド化されたライブラリを使っても性能は上がらないことを示唆しています。実時間の大幅な短縮を期待してください。並行プログラミングにおけるこれらの概念は、それぞれCPU時間とウォールクロック時間と呼ばれます。

マルチプロセッシング・ライブラリ
 

最新のプロセッサーで利用可能なマルチコアをフル活用するために、マルチプロセッシング・ライブラリ 使用されます。これはスレッディング・ライブラリとは全く異なる方法で動作しますが、2つのライブラリの構文は非常によく似ています。

マルチプロセッシングライブラリは、実際には各並列タスクに対して複数のOSプロセスを生成します。各プロセスに個別の Python インタプリタと個別のグローバルな解釈ロックを与えることで、グローバルな解釈ロックに関連する問題をうまく回避できます。さらに、各プロセスは別々のプロセッサコアを占有することができ、すべてのプロセスが処理を終えたときに結果を再編成することができます。

しかし、いくつかの欠点もあります。複数のプロセッサによるデータ処理がデータの乱雑さを引き起こす可能性があるためです。これは全体的な実行時間の増加につながります。しかし、データを各プロセスに限定することを前提にすれば、パフォーマンスを大幅に向上させることは可能です。もちろん、いくら改善してもアムダールの法則定める限界を超えることはありません。

Pythonの実装

マルチプロセシングを使用した実装では、インポート行とマルチプロセシングの変更のみが必要です。ターゲット関数へのパラメータはここで別途渡します。それ以外はThreadingの実装とほとんど同じです:

# multiproc_test.pyimport randomimport multiprocessingdef list_append(count, id, out_list):  
    """  
    Creates an empty list and then appends a   
    random number to the list 'count' number  
    of times. A CPU-heavy operation!  
    """ 
    for i in range(count):  
        out_list.append(random.random())if __name__ == "__main__":  
    size = 10000000   # Number of random numbers to add  
    procs = 2   # Number of processes to create  
 
    # Create a list of jobs and then iterate through  
    # the number of processes appending each process to  
    # the job list   
    jobs = []  
    for i in range(0, procs):  
        out_list = list()  
        process = multiprocessing.Process(target=list_append,   
                                          args=(size, i, out_list))  
        jobs.append(process)  
 
    # Start the processes (i.e. calculate the random number lists)        
    for j in jobs:  
        j.start()  
 
    # Ensure all of the processes have finished  
    for j in jobs:  
        j.join()  
 
    print "List processing complete." 

コンソールテストの実行時間:

time python multiproc_test.py 

次のような出力が得られます:

List processing complete.  
real    0m1.045s 
user    0m1.824s 
sys     0m0.231s 

この例では、userとsysの時間は基本的に同じですが、realの時間はほぼ2分の1に低下していることがわかります。これは、2つのプロセスが使用されているために起こります。プロセスを4つに拡張したり、リストの長さを半分にすると、次のようになります:

List processing complete.  
real    0m0.540s 
user    0m1.792s 
sys     0m0.269s 

4つのプロセスを使用することで、スピードは約3.8倍に向上しました。しかし、このルールをより大規模で複雑なプログラムに一般化するには注意が必要です。データ変換、ハードウェアのカチャ・レベル、その他多くの問題がスピードアップの妨げになる可能性があります。

関連記事

  • PythonとNumPyによるコレスキー分解

  • Pythonによるヨーロピアン・バニラ・コール・プット・オプションのプライシング

  • PythonとNumPyのヤコビ法

  • PythonとNumPyによるLU分解

  • Pythonでオプション価格決定

  • PythonとNumPyによるQR分解

  • Ubuntu 14.04でのPython定量調査環境のクイックスタート

Read next

エンタープライズWLANの開発動向

スマート端末の急速な普及とモバイルインターネットサービスの急速な発展により、WLANは前進を続けており、今日、WLANは企業や家庭でいたるところに存在し、ユーザーにとってインターネットへのアクセスの主流となっています。企業向けワイヤレス・サービスの継続的な多様化の中で、企業向け WLAN もまた多様化しています。

Oct 5, 2014 · 5 min read