簡體   English   中英

Python for 循環:多處理的正確實現

[英]Python for loop: Proper implementation of multiprocessing

以下 for 循環是迭代模擬過程的一部分,是計算時間的主要瓶頸:

import numpy as np

class Simulation(object):

    def __init__(self,n_int):
        self.n_int = n_int

    def loop(self):

        for itr in range(self.n_int):        
            #some preceeding code which updates rows_list and diff with every itr
            cols_red_list = []
            rows_list = list(range(2500)) #row idx for diff where negative element is known to appear
            diff = np.random.uniform(-1.323, 3.780, (2500, 300)) #np.random.uniform is just used as toy example 

            for row in rows_list:
                col =  next(idx for idx, val in enumerate(diff[row,:]) if val < 0)
                cols_red_list.append(col)
            # some subsequent code which uses the cols_red_list data    
sim1 = Simulation(n_int=10)
sim1.loop()

因此,我嘗試通過使用multiprocessing包來並行化它,以期減少計算時間:

import numpy as np
from multiprocessing import  Pool, cpu_count
from functools import partial

def crossings(row, diff):
    return next(idx for idx, val in enumerate(diff[row,:]) if val < 0)

class Simulation(object): 
    def __init__(self,n_int):
        self.n_int = n_int

    def loop(self):        
        for itr in range(self.n_int): 
            #some preceeding code which updates rows_list and diff with every
            rows_list = list(range(2500))
            diff = np.random.uniform(-1, 1, (2500, 300))

            if __name__ == '__main__':
                num_of_workers = cpu_count()
                print('number of CPUs : ', num_of_workers)
                pool = Pool(num_of_workers)
                cols_red_list = pool.map(partial(crossings,diff = diff), rows_list)
                pool.close()
                print(len(cols_red_list))
            # some subsequent code which uses the cols_red_list data 

sim1 = Simulation(n_int=10)
sim1.loop()

不幸的是,與順序代碼相比,並行化要慢得多。 因此我的問題是:在那個特定的例子中我是否正確使用了 multiprocessing 包? 是否有其他方法可以並行化上述 for 循環?

免責聲明:當您試圖通過並行化減少代碼的運行時間時,這並不能嚴格回答您的問題,但它可能仍然是一個很好的學習機會。

作為黃金法則,在轉向多處理以提高性能(執行時間)之前,應該首先優化單線程情況。

您的

rows_list = list(range(2500))

生成數字02499 (即range )並將它們存儲在內存中( list ),這需要時間來分配所需的內存和實際寫入。 然后,您只使用這些可預測值一次,方法是從內存中讀取它們(這也需要時間),以可預測的順序:

for row in rows_list:

當您重復執行時,這與loop函數的運行時特別相關( for itr in range(n_int): )。

相反,請考慮僅在需要時生成數字,而無需中間存儲(這在概念上消除了訪問 RAM 的任何需要):

for row in range(2500):

其次,除了共享相同的問題(對內存的不必要訪問)之外,還有以下內容:

diff = np.random.uniform(-1, 1, (2500, 300))
# ...
    col =  next(idx for idx, val in enumerate(diff[row,:]) if val < 0)

在我看來,在數學(或邏輯)層面上是可以優化的。

您要做的是通過將其定義為“我第一次在 [-1;1] 中遇到低於 0 的隨機變量”來獲得一個隨機變量(即col索引)。 但是請注意,確定在 [-α;α] 上具有均勻分布的隨機變量是否為負,與在 {0,1} 上(bool )具有隨機變量相同。

因此,您現在使用的是bool s 而不是float s,您甚至不必進行比較( val < 0 ),因為您已經有了 bool 。 這可能會使代碼更快。 使用與rows_list相同的想法,您可以僅在需要時生成該bool 測試它直到它為True (或False ,選擇一個,這顯然無關緊要)。 通過這樣做,你只生成你需要的隨機bool s,不多也不少(順便說一句,如果行中的所有 300 個元素都是負數,你的代碼會發生什么?;)):

for _ in range(n_int):
    cols_red_list = []
    for row in range(2500):
        col = next(i for i in itertools.count() if random.getrandbits(1))
        cols_red_list.append(col)

或者,使用列表理解:

cols_red_list = [next(i for i in count() if getrandbits(1))
                 for _ in range(2500)]

我敢肯定,通過適當的統計分析,您甚至可以將col隨機變量表示為 [0; 上的非均勻變量; limit [,讓您可以更快地計算它。

請先測試您的單線程實現的“優化”版本的性能。 如果運行時仍然不可接受,那么您應該研究多線程。

multiprocessing使用系統進程(而不是線程!)進行並行化,這需要昂貴的 IPC(進程間通信)來共享數據。

這在兩個方面咬你:

  • diff = np.random.uniform(-1, 1, (2500, 300))創建一個大矩陣,這對於pickle/復制到另一個進程來說很昂貴
  • rows_list = list(range(2500))創建一個較小的列表,但這里同樣適用。

為了避免這種昂貴的 IPC,您有一個半選擇:

  • 如果在符合 POSIX 的系統上,請在模塊級別初始化變量,這樣每個進程都會獲得所需數據的快速副本。 這是不可擴展的,因為它需要 POSIX、奇怪的架構(您可能不想將所有內容都放在模塊級別),並且不支持共享對該數據的更改。
  • 使用共享內存。 這僅支持大部分原始數據類型,但mp.Array應該可以滿足您的需求。

第二個問題是設置池很昂貴,因為需要啟動num_cpu進程。 與此開銷相比,您的工作負載小到可以忽略不計。 一種好的做法是只創建一個池並重用它。

這是僅 POSIX 解決方案的一個簡單粗暴的示例:

import numpy as np
from multiprocessing import  Pool, cpu_count
from functools import partial

n_int = 10

rows_list = np.array(range(2500))
diff = np.random.uniform(-1, 1, (2500, 300))


def crossings(row, diff):
    return next(idx for idx, val in enumerate(diff[row,:]) if val < 0)

def workload(_):
    cols_red_list = [crossings(row, diff) for row in rows_list]
    print(len(cols_red_list))


class Simulation(object):

    def loop(self):
        num_of_workers = cpu_count() 
        with Pool(num_of_workers) as pool:
            pool.map(workload, range(10))
        pool.close()

sim1 = Simulation()
sim1.loop()

對我(和我的兩個核心)來說,這大約是順序版本的兩倍。

使用共享內存更新:

import numpy as np
from multiprocessing import  Pool, cpu_count, Array
from functools import partial

n_int = 10

ROW_COUNT = 2500


### WORKER

diff = None
result = None

def init_worker(*args):
    global diff, result
    (diff, result) = args


def crossings(i):
    result[i] = next(idx for idx, val in enumerate(diff[i*300:(i+1)*300]) if val < 0)


### MAIN

class Simulation():
    def loop(self):
        num_of_workers = cpu_count() 

        diff = Array('d', range(ROW_COUNT*300), lock=False)
        result = Array('i', ROW_COUNT, lock=False)
        # Shared memory needs to be passed when workers are spawned
        pool = Pool(num_of_workers, initializer=init_worker, initargs=(diff, result))

        for i in range(n_int):

            # SLOW, I assume you use a different source of values anyway.
            diff[:] = np.random.uniform(-1, 1, ROW_COUNT*300)

            pool.map(partial(crossings), range(ROW_COUNT))
            print(len(result))
        pool.close()


sim1 = Simulation()
sim1.loop()

一些注意事項:

  • 共享內存需要在創建工作線程時設置,因此無論如何它都是全局的。
  • 這仍然不比順序版本快,但這主要是因為 random.uniform 需要完全復制到共享內存中。 我認為這只是用於測試的值,實際上您無論如何都會以不同的方式填充它。
  • 我只將索引傳遞給工作人員,並使用它們來讀取和寫入共享內存的值。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM