2016-01-28 8 views
9

は、次の関数を考えてみましょう:Pythonマルチプロセッシング - functools.partialをデフォルト引数よりも遅く使用するのはなぜですか?

def f(x, dummy=list(range(10000000))): 
    return x 

私はmultiprocessing.Pool.imapを使用している場合は、私は次のタイミングを得る:

import time 
import os 
from multiprocessing import Pool 

def f(x, dummy=list(range(10000000))): 
    return x 

start = time.time() 
pool = Pool(2) 
for x in pool.imap(f, range(10)): 
    print("parent process, x=%s, elapsed=%s" % (x, int(time.time() - start))) 

parent process, x=0, elapsed=0 
parent process, x=1, elapsed=0 
parent process, x=2, elapsed=0 
parent process, x=3, elapsed=0 
parent process, x=4, elapsed=0 
parent process, x=5, elapsed=0 
parent process, x=6, elapsed=0 
parent process, x=7, elapsed=0 
parent process, x=8, elapsed=0 
parent process, x=9, elapsed=0 

今、私が代わりにデフォルト値を使用してのfunctools.partialを使用する場合:

import time 
import os 
from multiprocessing import Pool 
from functools import partial 

def f(x, dummy): 
    return x 

start = time.time() 
g = partial(f, dummy=list(range(10000000))) 
pool = Pool(2) 
for x in pool.imap(g, range(10)): 
    print("parent process, x=%s, elapsed=%s" % (x, int(time.time() - start))) 

parent process, x=0, elapsed=1 
parent process, x=1, elapsed=2 
parent process, x=2, elapsed=5 
parent process, x=3, elapsed=7 
parent process, x=4, elapsed=8 
parent process, x=5, elapsed=9 
parent process, x=6, elapsed=10 
parent process, x=7, elapsed=10 
parent process, x=8, elapsed=11 
parent process, x=9, elapsed=11 

functools.partialを使用しているバージョンの方がずっと遅いのはなぜですか?

+0

なぜ 'list(range(...)) 'を使用していますか? AFAIKあなたのコードは、ShadowRangerによって説明された問題が起こらず、酸洗いのオーバーヘッドがずっと小さくなることを除いて、 'list'を呼び出すことなく全く同じことをするでしょう。 – Bakuriu

+0

サイドノート:デフォルトの(または部分的にバインドされた)引数として 'list's(または他の変更可能な型)を使用することは危険です。なぜなら_same_' list'は新しいコピーではなく、関数のすべてのデフォルト呼び出し間で共有されるからです各呼び出しごとに。通常、新鮮なコピーが必要です。 – ShadowRanger

+0

注意しておきますが、通常、変更可能なオブジェクトをデフォルト値として使用することは好ましくありません。なぜなら、関数の中でそれを変更すると、関数への呼び出しごとに変更が表示されるからです。 – Copperfield

答えて

8

multiprocessingを使用するには、渡す引数だけでなく、実行する関数についての情報をワーカープロセスに送信する必要があります。その情報は、メインプロセスの情報であるpicklingによって転送され、それがワーカープロセスに送信され、そこで処理されません。

これは、主な問題につながる:

デフォルト引数を持つ関数を酸洗いは安いです。それは関数の名前だけをピックルします(そしてPythonに関数であることを知らせる情報)。ワーカープロセスは名前のローカルコピーを検索するだけです。彼らはすでに見つけるために名前付き関数fを持っているので、それを渡すのにほとんど何も費やされません。

しかし(デフォルトの引数はlist長い10Mとき高価)基本的な機能(安い)とすべてデフォルト引数を酸洗partial機能を必要とする酸洗。だから、タスクがpartialケースにディスパッチされるたびに、それはバインドされた引数を取り除き、それをワーカープロセスに送信し、ワーカープロセスはunpickleし、最後に "本当の"作業を行います。私のマシンでは、そのピックルのサイズは約50 MBで、これは膨大なオーバーヘッドです。私のマシンでの迅速なタイミングテストでは、のには約620ミリ秒かかります(50 MBのデータを実際に転送するオーバーヘッドは無視されます)。

partial彼らは自分の名前を知らないので、この方法でピックルしなければなりません。 fdef -ed)のような関数をpicklingすると、インタラクティブなインタプリタやプログラムのメインモジュール(__main__.f)の修飾名を知ることができるので、リモート側では、 from __main__ import f。しかし、partialはその名前を知らない。確かに、それをgに割り当てましたが、picklepartialもそれ自体が修飾名__main__.gで利用可能であることはわかりません。それはfoo.fredまたは百万の他のものと名付けることができます。だからそれは完全にゼロから再作成するために必要な情報をpickleに持っていなければなりません。また、作業項目間の親で呼び出し可能変数が変更されていないことを知らないため、毎回pickleを呼び出します(作業者ごとに1回だけではありません)。

その他の問題があります(listのタイミングの作成はpartialケースのみで、partialラップされた関数関数を直接呼び出す)が、呼び出しごとのオーバーヘッドのピクチャリングとアンピクルに関連した変更であることを示しています。listの初期作成では、少し下のオーバーヘッドを少し下に追加します。それぞれ pickle/unpickleサイクルコスト; partialを介して呼び出すオーバーヘッドは1マイクロ秒未満です)。

+0

1)デフォルト引数 'dummy'がピクルされていない場合、それはどのようにワーカーに送られますか?それはグローバル変数ではありませんか? 2) 'partial'では、各関数呼び出しは高価です。 'g'が各関数呼び出しのために(再)節約されることを意味しますか? –

+0

@usualme:#1:Linuxでは、ワーカーは親からフォークされているので、自分たちのメモリ空間に自分自身のコピーを持っています(コピーオンライトなので、実際には親はしばらくの間)。それらのコピーには既に初期化されているデフォルトの引数があります。修飾された名前で同じ関数を検索すると、すでに設定されています。 Windowsでは、Pythonはメインモジュールとして実行されているかのように実行して、 '__main__'を実行してforkをシミュレートします。関数が '__main__'でインポートされた場合、リストを作成するためのコストは、作業ではなく作業者ごとに1回支払われます。 – ShadowRanger

+0

@usualme:#2:うん、プールは一般的なものなので、ワーカープロセスが死んでも置き換えられないという保証はなく、ワーカーの結果を起動して受け取るプロセスは、渡された呼び出し可能コードを突然変異させないある与えられた作業者がまだ作業を受けていないこと、あるいは異なる呼び出し可能なものを使っている他のタスクが散在していない可能性があることなどを意味します。通常、コールバックはシリアル化するのにかなり安いですが、これは一般的なルールの例外である病理学的ケースの1つです。 – ShadowRanger

関連する問題