web-dev-qa-db-ja.com

multiprocessing.Pool()は通常の関数を使用するよりも遅い

(この質問は、multiprocessing.Pool()でコードをより速く実行する方法に関するものです。私はようやくそれを解決しました。最終的な解決策は投稿の下部にあります。)

元の質問:

Pythonを使用して、Wordをリスト内の他の多くの単語と比較し、最も類似する単語のリストを取得しようとしています。そのために、difflib.get_close_matches関数を使用しています。 Python 2.6.5を使用した、比較的新しく強力なWindows 7ラップトップコンピューターを使用しています。

単語の比較リストが非常に長く、比較プロセスを数回繰り返す必要があるため、比較プロセスを高速化したいのです。マルチプロセッシングモジュールについて聞いたとき、比較をワーカータスクに分割して同時に実行できる場合(つまり、高速化と引き換えにマシンの電力を利用できる場合)、比較タスクが速く終了するのは当然のように思えました。

ただし、さまざまな方法を試し、ドキュメントに示され、フォーラムの投稿で提案されているメソッドを使用した後でも、Poolメソッドは信じられないほど遅く、リスト全体で元のget_close_matches関数を実行するよりもはるかに遅いようです一度。 Pool()の処理速度が遅い理由と、それを正しく使用しているかどうかを理解するのに役立ちます。この文字列比較シナリオを例として使用しているのは、それが最新の例であるため、マルチプロセッシングを理解できなかったり、マルチプロセッシングが動作したりすることができなかったからです。以下は、difflibシナリオのコード例であり、通常の方法とプールされた方法の時間差を示しています。

from multiprocessing import Pool
import random, time, difflib

# constants
wordlist = ["".join([random.choice([letter for letter in "abcdefghijklmnopqersty"]) for lengthofword in xrange(5)]) for nrofwords in xrange(1000000)]
mainword = "hello"

# comparison function
def findclosematch(subwordlist):
    matches = difflib.get_close_matches(mainword,subwordlist,len(subwordlist),0.7)
    if matches <> []:
        return matches

# pool
print "pool method"
if __name__ == '__main__':
    pool = Pool(processes=3)
    t=time.time()
    result = pool.map_async(findclosematch, wordlist, chunksize=100)
    #do something with result
    for r in result.get():
        pass
    print time.time()-t

# normal
print "normal method"
t=time.time()
# run function
result = findclosematch(wordlist)
# do something with results
for r in result:
    pass
print time.time()-t

検索される単語は「こんにちは」であり、近い一致を見つける単語のリストは、ランダムに結合された5つの文字の100万の長いリストです(説明のみを目的としています)。私は3つのプロセッサコアと100のチャンクサイズ(リストアイテムはワーカーごとに処理されると思いますか?)のマップ関数を使用します(1000と10 000のチャンクサイズも試しましたが、実際の違いはありませんでした)。どちらの方法でも、関数を呼び出す直前にタイマーを開始し、結果をループした直後にタイマーを終了しています。以下に示すように、タイミング結果は明らかに元の非プールメソッドを支持しています。

>>> 
pool method
37.1690001488 seconds
normal method
10.5329999924 seconds
>>> 

プール方式は、元の方式よりもほぼ4倍遅くなります。ここに欠けているものはありますか、またはプール/マルチプロセッシングの動作について誤解している可能性がありますか?ここでの問題の一部は、map関数がNoneを返し、実際の一致のみが結果に返されて関数にそのように記述されていても、何千もの不要なアイテムが結果リストに追加されることが考えられます。私が理解していることから、それはマップがどのように機能するかということです。私は、False以外の結果のみを収集するfilterなどの他のいくつかの関数について聞いたことがありますが、multiprocessing/Poolがfilterメソッドをサポートしているとは思いません。マルチプロセッシングモジュールのmap/imap以外に、関数が返すものだけを返すのに役立つ関数はありますか? Apply関数は、私が理解しているように、複数の引数を与えるためのものです。

私が試したが、時間を改善することなくimap関数もあることを知っています。その理由は、「電光石火」と思われるitertoolsモジュールの優れた点を理解するのに問題があったのと同じ理由ですが、これは関数の呼び出しに当てはまることに気付きましたが、私の経験と私が読んだことから、関数を呼び出すことは実際には計算を行わないため、結果を反復処理して結果を収集および分析するとき(これがないと、条件を呼び出す意味がありません)は、関数の通常バージョンをそのまま使用します。しかし、それは別の投稿のためだと思います。

とにかく、誰かが私をここで正しい方向に微調整できるかどうかを見て興奮し、これに関する助けを本当に感謝します。この例を機能させるよりも、一般的にマルチプロセッシングを理解することに関心がありますが、理解を助けるためにいくつかのソリューションコードの例を提案すると役立ちます。

答え:

スローダウンは、追加のプロセスの起動時間が遅いことに関係しているようです。 .Pool()関数を十分な速度で取得できませんでした。より速くするための私の最終的な解決策は、手動でワークロードリストを分割し、.Pool()の代わりに複数の.Process()を使用して、ソリューションをキューに返すことでした。しかし、おそらく最も重要な変更は、比較する単語ではなく、検索するメインのWordに関してワークロードを分割していたのではないかと思います。おそらく、difflib検索機能はすでに非常に高速であるためです。これは、5つのプロセスを同時に実行する新しいコードで、単純なコードを実行するよりも約10倍速くなりました(6秒vs 55秒)。 difflibがすでにどれだけ高速であるかに加えて、高速のあいまい検索に非常に役立ちます。

from multiprocessing import Process, Queue
import difflib, random, time

def f2(wordlist, mainwordlist, q):
    for mainword in mainwordlist:
        matches = difflib.get_close_matches(mainword,wordlist,len(wordlist),0.7)
        q.put(matches)

if __name__ == '__main__':

    # constants (for 50 input words, find closest match in list of 100 000 comparison words)
    q = Queue()
    wordlist = ["".join([random.choice([letter for letter in "abcdefghijklmnopqersty"]) for lengthofword in xrange(5)]) for nrofwords in xrange(100000)]
    mainword = "hello"
    mainwordlist = [mainword for each in xrange(50)]

    # normal approach
    t = time.time()
    for mainword in mainwordlist:
        matches = difflib.get_close_matches(mainword,wordlist,len(wordlist),0.7)
        q.put(matches)
    print time.time()-t

    # split work into 5 or 10 processes
    processes = 5
    def splitlist(inlist, chunksize):
        return [inlist[x:x+chunksize] for x in xrange(0, len(inlist), chunksize)]
    print len(mainwordlist)/processes
    mainwordlistsplitted = splitlist(mainwordlist, len(mainwordlist)/processes)
    print "list ready"

    t = time.time()
    for submainwordlist in mainwordlistsplitted:
        print "sub"
        p = Process(target=f2, args=(wordlist,submainwordlist,q,))
        p.Daemon = True
        p.start()
    for submainwordlist in mainwordlistsplitted:
        p.join()
    print time.time()-t
    while True:
        print q.get()
25
Karim Bahgat

私の推測では、プロセス間通信(IPC)のオーバーヘッドです。単一プロセスのインスタンスでは、単一プロセスにWordリストがあります。他のさまざまなプロセスに委任する場合、メインプロセスは常にリストのセクションを他のプロセスにシャトルする必要があります。

したがって、より良いアプローチは、スピンオフnプロセスであり、それぞれがリストのセグメントのロード/生成1/nを担当し、 Wordはリストのその部分にあります。

ただし、Pythonのマルチプロセッシングライブラリでそれを行う方法はわかりません。

8
Multimedia Mike

これらの問題は通常、次のように要約されます。

並列化しようとしている関数は、並列化を合理化するのに十分なCPUリソース(つまり、CPU時間)を必要としません!

確かに、multiprocessing.Pool(8)で並列化すると、理論的には(しかし実際にはありません)8xスピードアップ。

ただし、これは無料ではないことに注意してください。次のオーバーヘッドを犠牲にしてこの並列化を実現します。

  1. Pool.map(f, iter)に渡されるtaskchunk(サイズchunksize)ごとにiterを作成する
  2. task ごとに
    1. tasktask'sの戻り値をシリアル化します(thinkpickle.dumps()
    2. tasktask'sの戻り値をデシリアライズします(thinkpickle.loads()
    3. ワーカープロセスと親プロセスget()およびput()がこれらのLocksとの間で共有している間、共有メモリQueuesQueuesを待機するのにかなりの時間を費やします。
  3. 各ワーカープロセスのos.fork()への呼び出しの1回限りのコスト。

本質的に、Pool()を使用する場合、次のことが必要です。

  1. 高いCPUリソース要件
  2. 各関数呼び出しに渡されるデータフットプリントが少ない
  3. 上記(3)の1回限りのコストを正当化するために、合理的に長いiter

より詳細な調査については、この投稿とリンクされたトークPool.map()および友達に渡されるデータの大きさのウォークスルー)トラブルに巻き込まれます。

レイモンドヘッティンガーは、Pythonの同時実行の適切な使用についてもここで説明しています。

7
The Aelfinn

私は別の問題でプールと同様の何かを経験しました。現時点では、実際の原因はわかりません...

The Answer OPによる編集Karim Bahgatは、私にとって有効なソリューションと同じです。 Process&Queueシステムに切り替えた後、マシンのコア数に合わせてスピードアップを確認できました。

ここに例があります。

def do_something(data):
    return data * 2

def consumer(inQ, outQ):
    while True:
        try:
            # get a new message
            val = inQ.get()

            # this is the 'TERM' signal
            if val is None:
                break;

            # unpack the message
            pos = val[0]  # its helpful to pass in/out the pos in the array
            data = val[1]

            # process the data
            ret = do_something(data)

            # send the response / results
            outQ.put( (pos, ret) )


        except Exception, e:
            print "error!", e
            break

def process_data(data_list, inQ, outQ):
    # send pos/data to workers
    for i,dat in enumerate(data_list):
        inQ.put( (i,dat) )

    # process results
    for i in range(len(data_list)):
        ret = outQ.get()
        pos = ret[0]
        dat = ret[1]
        data_list[pos] = dat


def main():
    # initialize things
    n_workers = 4
    inQ = mp.Queue()
    outQ = mp.Queue()
    # instantiate workers
    workers = [mp.Process(target=consumer, args=(inQ,outQ))
               for i in range(n_workers)]

    # start the workers
    for w in workers:
        w.start()

    # gather some data
    data_list = [ d for d in range(1000)]

    # lets process the data a few times
    for i in range(4):
        process_data(data_list)

    # tell all workers, no more data (one msg for each)
    for i in range(n_workers):
        inQ.put(None)
    # join on the workers
    for w in workers:
        w.join()

    # print out final results  (i*16)
    for i,dat in enumerate(data_list):
        print i, dat
1
verdverm