9

この例から始めて、さまざまな使用方法を試してみたかったmultiprocessing:

$ cat multi_bad.py 
import multiprocessing as mp
from time import sleep
from random import randint

def f(l, t):
#   sleep(30)
    return sum(x < t for x in l)

if __name__ == '__main__':
    l = [randint(1, 1000) for _ in range(25000)]
    t = [randint(1, 1000) for _ in range(4)]
#   sleep(15)
    pool = mp.Pool(processes=4)
    result = pool.starmap_async(f, [(l, x) for x in t])
    print(result.get())

以下lは、4 つのプロセスが生成されたときに 4 回コピーされるリストです。これを回避するために、ドキュメント ページでは、 を使用して作成されたキュー、共有配列、またはプロキシ オブジェクトの使用が提供されていmultiprocessing.Managerます。最後の 1 つについては、次の定義を変更しましたl

$ diff multi_bad.py multi_good.py 
10c10,11
<     l = [randint(1, 1000) for _ in range(25000)]
---
>     man = mp.Manager()
>     l = man.list([randint(1, 1000) for _ in range(25000)])

結果はまだ正しいように見えますが、実行時間が劇的に増加したため、何か間違ったことをしていると思います。

$ time python multi_bad.py 
[17867, 11103, 2021, 17918]

real    0m0.247s
user    0m0.183s
sys 0m0.010s

$ time python multi_good.py 
[3609, 20277, 7799, 24262]

real    0m15.108s
user    0m28.092s
sys 0m6.320s

ドキュメントには、この方法は共有配列よりも遅いと書かれていますが、これは間違っているように感じます。また、これをプロファイルして、何が起こっているかについての詳細情報を取得する方法もわかりません。何か不足していますか?

PS共有配列を使用すると、時間が0.25秒未満になります。

PPS これは Linux と Python 3.3 上にあります。

4

2 に答える 2

9

Linux は、サブプロセスがedのときにコピー オン ライトを使用します。os.forkデモンストレーションするには:

import multiprocessing as mp
import numpy as np
import logging
import os

logger = mp.log_to_stderr(logging.WARNING)

def free_memory():
    total = 0
    with open('/proc/meminfo', 'r') as f:
        for line in f:
            line = line.strip()
            if any(line.startswith(field) for field in ('MemFree', 'Buffers', 'Cached')):
                field, amount, unit = line.split()
                amount = int(amount)
                if unit != 'kB':
                    raise ValueError(
                        'Unknown unit {u!r} in /proc/meminfo'.format(u = unit))
                total += amount
    return total

def worker(i):
    x = data[i,:].sum()    # Exercise access to data
    logger.warn('Free memory: {m}'.format(m = free_memory()))

def main():
    procs = [mp.Process(target = worker, args = (i, )) for i in range(4)]
    for proc in procs:
        proc.start()
    for proc in procs:
        proc.join()

logger.warn('Initial free: {m}'.format(m = free_memory()))
N = 15000
data = np.ones((N,N))
logger.warn('After allocating data: {m}'.format(m = free_memory()))

if __name__ == '__main__':
    main()

それがもたらした

[WARNING/MainProcess] Initial free: 2522340
[WARNING/MainProcess] After allocating data: 763248
[WARNING/Process-1] Free memory: 760852
[WARNING/Process-2] Free memory: 757652
[WARNING/Process-3] Free memory: 757264
[WARNING/Process-4] Free memory: 756760

これは、最初に約 2.5GB の空きメモリがあったことを示しています。の 15000x15000 配列を割り当てた後、float64763248 KB の空き容量がありました。15000**2*8 バイト = 1.8GB であり、メモリの減少である 2.5GB - 0.763248GB もおよそ 1.8GB であるため、これはおおよそ理にかなっています。

現在、各プロセスが生成された後、空きメモリは約 750MB であると再度​​報告されます。空きメモリが大幅に減少していないため、システムがコピー オン ライトを使用しているに違いないと結論付けました。

結論: データを変更する必要がない場合、__main__モジュールのグローバル レベルでデータを定義することは、サブプロセス間でデータを共有するための便利で (少なくとも Linux では) メモリに優しい方法です。

于 2012-10-29T19:35:28.143 に答える
6

共有オブジェクトにアクセスするということは、ある種のシグナル/システムコールを通じてリクエストをピクルして送信しなければならないことを意味するため、これは予想されることです。

基本的に、メモリをできるだけ共有しないようにする必要があります。これにより、コードのデバッグが容易になり (同時実行性が大幅に低下するため)、速度が向上します。

共有メモリは、本当に必要な場合にのみ使用する必要があります (たとえば、ギガバイトのデータを共有してコピーすると大量の RAM が必要になる場合や、プロセスがこの共有メモリを介して対話できるようにする必要がある場合など)。

余談ですが、マネージャーは PyObject * を処理できる必要があり、ピクル/アンピクルなどを行う必要があるため、マネージャーの使用は共有配列よりもはるかに遅くなりますが、配列はこのオーバーヘッドの多くを回避できます。

マルチプロセッシングのドキュメントから:

マネージャーは、異なるプロセス間で共有できるデータを作成する方法を提供します。マネージャー オブジェクトは、共有オブジェクトを管理するサーバー プロセスを制御します。他のプロセスは、プロキシを使用して共有オブジェクトにアクセスできます。

そのため、マネージャーを使用すると、共有メモリを処理するためだけに使用される新しいプロセスが生成されます。おそらく、それがより多くの時間がかかる理由です。

プロキシの速度をプロファイリングしようとすると、共有されていないリストよりもはるかに遅くなります。

>>> import timeit
>>> import multiprocessing as mp
>>> man = mp.Manager()
>>> L = man.list(range(25000))
>>> timeit.timeit('L[0]', 'from __main__ import L')
50.490395069122314
>>> L = list(range(25000))
>>> timeit.timeit('L[0]', 'from __main__ import L')
0.03588080406188965
>>> 50.490395069122314 / _
1407.1701119638526

anArrayはそれほど遅くはありませんが、次のようになります。

>>> L = mp.Array('i', range(25000))
>>> timeit.timeit('L[0]', 'from __main__ import L')
0.6133401393890381
>>> 0.6133401393890381 / 0.03588080406188965
17.09382371507359

非常に初歩的な操作は遅く、高速化の見込みはあまりないと考えられるため、大量のデータ リストを共有する必要があり、高速にアクセスしたい場合は、 を使用する必要がありArrayます。

一度に複数の要素にアクセスすることで (単一の要素ではなくスライスを取得するなど)、処理が少し速くなる可能性がありますが、何をしたいかによって、これが可能である場合と不可能である場合があります。

于 2012-10-29T12:43:12.840 に答える