2016-01-28 25 views
9

Si consideri la seguente funzione:Multiprocessing di Python - Perché usare functools.partial più lentamente degli argomenti predefiniti?

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

Se uso multiprocessing.Pool.imap, ottengo i seguenti orari:

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 

Ora, se io uso functools.partial invece di utilizzare un valore di default:

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 

Perché la versione che utilizza functools.partial è molto più lenta?

+0

Perché stai usando 'lista (intervallo (...))'? AFAIK il tuo codice farebbe esattamente la stessa cosa senza la chiamata a 'list', tranne che il problema spiegato da ShadowRanger non si verificherebbe e il sovraccarico del pickling sarebbe * molto * più piccolo. – Bakuriu

+0

Side-note: Usare 'list's (o qualsiasi altro tipo mutable) come argomento predefinito (o' partial' bound) è pericoloso, dal momento che la _same_ 'lista' è condivisa tra tutte le invocazioni predefinite della funzione, non una nuova copia per ogni chiamata; di solito, vuoi la nuova copia. – ShadowRanger

+0

come nota a parte, di solito è una pessima idea usare l'oggetto mutevole come valori predefiniti perché se lo si modifica nella funzione ogni successiva chiamata alla funzione vedrà le modifiche – Copperfield

risposta

8

L'utilizzo di multiprocessing richiede l'invio delle informazioni sui processi del lavoratore sulla funzione da eseguire, non solo sugli argomenti da passare. Queste informazioni vengono trasferite da pickling le informazioni nel processo principale, inviandole al processo di lavoro e deselezionandole.

Questo porta al problema principale:

decapaggio una funzione con argomenti di default è a buon mercato; assorbe solo il nome della funzione (più le informazioni per far sapere a Python che è una funzione); il lavoratore elabora solo la copia locale del nome. Hanno già una funzione denominata f da trovare, quindi non costa quasi nulla passarlo.

Ma decapaggio funzione partial comporta decapaggio la funzione sottostante (economico) e tutti i parametri predefiniti (costoso quando l'argomento predefinito è un 10M lungo list). Quindi, ogni volta che un compito viene inviato nel caso partial, esso decodifica l'argomento associato, lo invia al processo di lavoro, il processo di lavoro despassa, infine esegue il lavoro "reale". Sulla mia macchina, il pickle ha una dimensione di circa 50 MB, che è un enorme ammontare di spese generali; in test di temporizzazione rapidi sulla mia macchina, il decapaggio e il disfacimento di una lunghezza di 10 milioni di list di 0 richiede circa 620 ms (e ciò ignora il sovraccarico del trasferimento effettivo dei 50 MB di dati).

partial s devono sottaceti in questo modo, perché non conoscono il proprio nome; quando decollando una funzione come f, f (essendo def -ed) conosce il suo nome qualificato (in un interprete interattivo o dal modulo principale di un programma, è __main__.f), quindi il lato remoto può semplicemente ricrearlo localmente facendo l'equivalente di from __main__ import f. Ma il partial non conosce il suo nome; certo, lo hai assegnato a g, ma né lo pickle né lo stesso partial lo conoscono disponibile con il nome qualificato __main__.g; potrebbe essere chiamato foo.fred o un milione di altre cose. Quindi deve pickle le informazioni necessarie per ricrearlo interamente da zero. È anche pickle -ing per ogni chiamata (non solo una volta per lavoratore) perché non sa che il callable non sta cambiando nel genitore tra gli elementi di lavoro, e sta sempre cercando di assicurarsi che invii lo stato aggiornato.

avete altri problemi (temporizzazione creazione del list solo nel caso partial e l'overhead minore di chiamare una funzione di partial avvolto vs.chiamando direttamente la funzione), ma si tratta di cambiamenti di chump relativi al picking overhead della chiamata e all'annullamento dello partial (la creazione iniziale dello list sta aggiungendo un overhead di poco meno della metà di ciò che ogni pickle/unpickle costi del ciclo, il sovraccarico per chiamare lo partial è inferiore a un microsecondo).

+0

1) Se l'argomento predefinito 'dummy' non è in pickled, come viene inviato all'operatore? Non è una variabile globale, vero? 2) Con 'partial', ogni chiamata di funzione è costosa. Significa che 'g' viene (re) decapitato per ogni chiamata di funzione? –

+0

@usualme: # 1: su Linux, gli operatori sono biforcati dal genitore, quindi hanno già la loro copia della funzione nel proprio spazio di memoria (è copy-on-write, quindi potrebbero effettivamente condividere pagine con il genitore per un po '). E la loro copia ha già lo stesso argomento predefinito inizializzato, quindi quando cercano la stessa funzione con il nome qualificato, viene già impostato. Su Windows, Python simula fork eseguendo '__main__' senza eseguirlo come se fosse eseguito come il modulo principale; se la funzione viene importata in '__main__', il costo per rendere la lista viene pagato una volta per lavoratore, non per attività. – ShadowRanger

+0

@usualme: # 2: Sì, il 'Pool' è generico, e non vi è alcuna garanzia che i processi di lavoro non moriranno e vengano sostituiti, che il processo di avvio e ricezione dei risultati da parte dei lavoratori non muti il ​​callable passato per 'imap', che ogni dato lavoratore ha ancora ricevuto un lavoro, o che altre attività che usano callable diversi potrebbero non essere intercalate, ecc. Quindi sia callable che gli argomenti sono serializzati per la spedizione su ogni singola attività, non solo una volta per lavoratore. Di solito, le callebles sono abbastanza economiche da serializzare, questo è uno di quei casi patologici che è l'eccezione alla regola generale. – ShadowRanger