2016-01-28 26 views
9

Rozważmy następującą funkcję:Wieloprocesorowość w języku Python - Dlaczego użycie functools.partial jest wolniejsze od domyślnych?

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

Jeśli używam multiprocessing.Pool.imap, otrzymuję następujące czasy:

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 

Teraz, jeśli mogę użyć functools.partial zamiast używać wartości domyślnej:

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 

Dlaczego wersja używająca functools.partial jest wolniejsza?

+0

Dlaczego używasz 'list (range (...))'? AFAIK twój kod zrobiłby dokładnie to samo bez wezwania do "listy", z wyjątkiem tego, że problem wyjaśniony przez ShadowRanger nie wystąpiłby, a narzut piklowania byłby * znacznie * mniejszy. – Bakuriu

+0

Notatka: Używanie 'list's (lub dowolnego innego typu zmiennego) jako argumentu domyślnego (lub" częściowego "związanego) jest niebezpieczne, ponieważ lista _same_' jest dzielona pomiędzy wszystkimi domyślnymi wywołaniami funkcji, a nie świeżą kopią dla każdego połączenia; zazwyczaj chcesz mieć świeżą kopię. – ShadowRanger

+0

jak na marginesie, jest zwykle zły pomysł używając zmiennego obiektu jako wartości domyślnych, ponieważ jeśli zmodyfikujesz go w funkcji, każde kolejne wywołanie funkcji będzie widoczne zmiany – Copperfield

Odpowiedz

8

Korzystanie z multiprocessing wymaga wysłania pracownikowi przetwarzania informacji o funkcji do uruchomienia, a nie tylko argumentów do przekazania. Informacje te są przesyłane przez pickling, które znajdują się w głównym procesie, wysyłając je do procesu roboczego i tam je rozpakowują.

Prowadzi to do podstawowej kwestii:

staliBejcowanie funkcji z domyślnych argumentów jest tani; tylko wypełnia nazwę funkcji (plus informacje, aby Python wiedział, że jest to funkcja); pracownik przetwarza tylko kopię lokalną nazwy. Mają już nazwaną funkcję f do znalezienia, więc jej przekazanie kosztuje prawie nic.

Ale kiszenia się partial funkcji polega na trawieniu funkcji podstawowej (tanie) i wszystkie argumenty domyślne (drogie gdy domyślny argument jest 10M długo list). Tak więc za każdym razem, gdy zadanie jest wysyłane w przypadku partial, polega ono na wytrawianiu związanego argumentu, wysyłaniu go do procesu roboczego, procesowi roboczemu unpickles, a następnie na końcu "prawdziwej" pracy. Na mojej maszynie ta marynarka ma około 50 MB, co stanowi ogromną ilość narzutów; w szybkich testach czasowych na moim komputerze, trawienie i rozpakowywanie 10 milionów długich list z 0 trwa około 620 ms (i to ignoruje narzut faktycznego przeniesienia 50 MB danych).

partial s muszą się tego nauczyć, ponieważ nie znają swoich nazw; podczas wytrawiania funkcji takiej jak f, f (będąc def -ed) zna swoją kwalifikowaną nazwę (w interaktywnym tłumaczu lub z głównego modułu programu, to jest __main__.f), więc strona zdalna może po prostu odtworzyć ją lokalnie, wykonując równoważnik from __main__ import f. Ale partial nie zna swojej nazwy; na pewno przypisałeś ją do g, ale ani pickle, ani sama partial nie wiedzą, że jest ona dostępna pod kwalifikowaną nazwą __main__.g; można go nazwać foo.fred lub milion innych rzeczy. Musi więc uzyskać informacje niezbędne do odtworzenia go całkowicie od zera. Jest również pickle -ing dla każdego połączenia (nie tylko raz na pracownika), ponieważ nie wie, że żądanie wywołania nie zmienia się w nadrzędnym między elementami pracy, i zawsze stara się upewnić, że wysyła aktualny stan.

Masz inne problemy (tworzenie taktowania list tylko w przypadku partial i niewielki narzut wywoływania funkcji opakowanej partialwywołanie funkcji bezpośrednio), ale są to zmiany typu "chump" w stosunku do pikowania narzutowego w trakcie wywołania i rozpakowania, które dodaje partial (początkowe utworzenie list dodaje jednorazowy narzut o wartości mniejszej niż połowa wartości każdego piksela/odkrycia koszty cyklu, koszt połączenia przez partial jest mniejszy niż mikrosekundę).

+0

1) Jeśli domyślny argument 'manekin' nie jest peklowany, to w jaki sposób jest wysyłany do pracownika? To nie jest zmienna globalna, prawda? 2) Przy "częściowym" każde wywołanie funkcji jest drogie. Czy to znaczy, że 'g' zostaje (re) wytrawiony dla każdego wywołania funkcji? –

+0

@wykluczme: # 1: W systemie Linux robotnicy są rozgałęzieni od rodzica, więc mają już własną kopię funkcji we własnej pamięci (jest ona kopiowana podczas zapisywania, więc mogą w rzeczywistości udostępniać strony rodzic przez chwilę). Ich kopia ma już ten sam domyślny argument, więc gdy sprawdzą tę samą funkcję według kwalifikowanej nazwy, zostanie ona już skonfigurowana. W systemie Windows Python symuluje fork, uruchamiając '__main__' bez uruchamiania go tak, jakby był uruchamiany jako główny moduł; jeśli funkcja jest importowana w '__main__', koszt sporządzenia listy jest płatny raz na pracownika, a nie zadanie. – ShadowRanger

+0

@wykluczme: # 2: Tak, 'Pula' jest ogólna i nie ma gwarancji, że procesy robocze nie umrą i nie zostaną zastąpione, że proces uruchamiania i otrzymywania wyników od pracowników nie zmutuje przekazanej liczby do 'imap', że dany pracownik otrzymał jeszcze pracę, lub że inne zadania używające różnych kalendarzy mogą nie być przeplatane, itp. Tak więc zarówno wywoływalne, jak i argumenty są serializowane do wysyłki w każdym pojedynczym zadaniu, a nie tylko raz na pracownika. Zazwyczaj kalki są dość tanie w serializacji, jest to jeden z tych patologicznych przypadków, które stanowią wyjątek od ogólnej zasady. – ShadowRanger