2016-01-28 8 views
9

Betrachten Sie die folgende Funktion:Python Multiprocessing - Warum ist functools.partial langsamer als Standardargumente?

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

Wenn ich multiprocessing.Pool.imap verwenden, erhalte ich die folgenden Timings:

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 

Nun, wenn ich functools.partial anstatt mit einem Standardwert verwenden:

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 

Warum ist die Version mit functools.partial so viel langsamer?

+0

Warum verwenden Sie 'Liste (Bereich (...))'? AFAIK Ihr Code würde genau dasselbe ohne den Aufruf von 'list' machen, außer dass das von ShadowRanger erklärte Problem nicht auftritt und der Aufwand für das Beizen viel * kleiner wäre. – Bakuriu

+0

Side-Anmerkung: Mit 'list's (oder einem anderen wandelbaren Typ) als Standard (oder' partial' gebunden) Argumente ist gefährlich, da die _same_ 'list' zwischen all Standard Anrufungen der Funktion freigegeben ist, nicht eine neue Kopie für jeden Anruf; Normalerweise möchten Sie die frische Kopie. – ShadowRanger

+0

als beiseite beachten Sie, dass in der Regel keine gute Idee veränderbares Objekt als Standardwerte verwenden, denn wenn man es in der Funktion auf die Funktion jeder nachfolgenden Aufruf ändern wird um die Änderungen sehen – Copperfield

Antwort

8

Die Verwendung von multiprocessing erfordert das Senden der Worker-Prozesse Informationen über die auszuführende Funktion, nicht nur die Argumente übergeben werden. Diese Information wird von pickling diese Information in dem Hauptprozess übertragen, es an den Arbeitsprozess senden und es dort entpacken.

Dies führt zum Hauptproblem:

eine Funktion mit Standardargumenten Beiz- ist billig; es pickt nur den Namen der Funktion (plus die Information, um Python wissen zu lassen, dass es eine Funktion ist); Die Worker-Prozesse sehen nur die lokale Kopie des Namens nach. Sie haben bereits eine benannte Funktion f zu finden, so dass es fast nichts kostet, sie zu passieren.

Aber Beizen eine partial Funktion beinhaltet das Beizen die zugrundeliegende Funktion (billig) und alle der Standardargumente (teuer, wenn das Standardargument eine 10M langen list ist). Jedes Mal, wenn eine Aufgabe im partial-Fall ausgelöst wird, wird das gebundene Argument gebeizt, an den Arbeitsprozess gesendet, der Arbeitsprozess wird unpickles ausgeführt, und schließlich wird die "echte" Arbeit ausgeführt. Auf meiner Maschine ist diese Beize etwa 50 MB groß, was eine enorme Menge an Aufwand bedeutet. Bei schnellen Timing-Tests an meiner Maschine dauert das Beizen und Entnehmen einer 10 Millionen langen list von etwa 620 ms (und das ignoriert den Overhead der tatsächlichen Übertragung der 50 MB Daten).

partial s müssen auf diese Weise pickle, weil sie ihre eigenen Namen nicht kennen; Wenn er eine Funktion wie f, f (def -ed) kennt, kennt er seinen qualifizierten Namen (in einem interaktiven Interpreter oder vom Hauptmodul eines Programms, es ist __main__.f), so dass die entfernte Seite es nur lokal wiederherstellen kann, indem es das Äquivalent von from __main__ import f. Aber die partial kennt ihren Namen nicht; sicher, Sie haben es g zugeordnet, aber weder pickle noch die partial selbst wissen, dass es mit dem qualifizierten Namen __main__.g; Es könnte foo.fred oder eine Million andere Dinge genannt werden. So muss es pickle die Informationen notwendig, um es vollständig von Grund auf neu zu erstellen. Es ist auch pickle -ing für jeden Aufruf (nicht nur einmal pro Worker), weil es nicht weiß, dass die aufrufbare nicht im übergeordneten zwischen Arbeitsaufgaben ändert, und es versucht immer sicherzustellen, dass es aktuelle Status sendet.

Sie haben andere Probleme (Timing-Erstellung der list nur in der partial Fall und der kleinere Overhead des Aufrufs einer partial umgebrochenen Funktion vs.Rufen Sie die Funktion direkt), aber das sind Chump Änderung im Vergleich zu den pro-Aufruf Overhead Beizen und Entpacken der partial wird hinzugefügt (die ursprüngliche Erstellung der list ist einmalige Zugabe von etwas unter der Hälfte jeweils pickle/unpickle Zykluskosten; der Overhead, der durch partial aufgerufen wird, ist weniger als eine Mikrosekunde).

+0

1) Wenn das Standardargument 'dummy' nicht gebeizt ist, wie wird es dann an den Arbeiter gesendet? Es ist keine globale Variable, oder? 2) Mit dem "Partial" ist jeder Funktionsaufruf teuer. Bedeutet dies, dass 'g' für jeden Funktionsaufruf (wieder) gebeizt wird? –

+0

@usualme: # 1: Unter Linux werden die Arbeiter von der Mutter gegabelt, so haben sie bereits ihre eigene Kopie der Funktion in ihrem eigenen Speicherbereich (es ist copy-on-write, so können sie tatsächlich mit dem teilen Seiten Eltern für eine Weile). Und ihre Kopie hat bereits das gleiche Standardargument initialisiert. Wenn sie die gleiche Funktion nach einem qualifizierten Namen suchen, ist sie bereits eingerichtet. Unter Windows simuliert Python fork, indem es "__main__" ausführt, ohne es so auszuführen, als würde es als Hauptmodul ausgeführt werden. Wenn die Funktion in '__main__' importiert wird, werden die Kosten für die Erstellung der Liste einmal pro Worker, nicht Task, bezahlt. – ShadowRanger

+0

@usualme: # 2: Yup, ist der 'Pool' generisch, und es gibt keine Garantie, dass Arbeitsprozesse werden nicht sterben und ersetzt werden, dass der Prozess der Einführung und Ergebnisse von den Arbeitern empfangen wird nicht die abrufbare mutieren weitergegeben zu "imap", dass ein beliebiger Arbeiter sogar Arbeit erhalten hat, oder dass andere Aufgaben, die unterschiedliche Callables verwenden, nicht eingestreut werden können. So werden sowohl aufrufbare als auch Argumente für den Versand bei jeder einzelnen Aufgabe serialisiert, nicht nur einmal pro Arbeiter. Gewöhnlich sind Callables ziemlich billig zu serialisieren, das ist einer dieser pathologischen Fälle, die die Ausnahme von der allgemeinen Regel sind. – ShadowRanger

Verwandte Themen