Multiprocesamiento de Python: ¿por qué es más lento utilizar los functools.partial que los argumentos predeterminados?

Considere la siguiente función:

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

Si uso multiprocessing.Pool.imap , obtengo los siguientes tiempos:

 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 

Ahora si uso functools.partial lugar de usar un valor predeterminado:

 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 

¿Por qué la versión que usa functools.partial es mucho más lenta?

El uso de multiprocessing requiere enviar al trabajador procesos información sobre la función para ejecutar, no solo los argumentos para pasar. Esa información se transfiere recogiendo esa información en el proceso principal, enviándola al proceso de trabajo y deshaciéndola allí.

Esto lleva al problema principal:

Decapar una función con argumentos predeterminados es barato ; solo esconde el nombre de la función (más la información para que Python sepa que es una función); Los procesos de trabajo solo buscan la copia local del nombre. Ya tienen una función nombrada f para encontrar, por lo que no cuesta casi nada pasarla.

Pero la selección partial una función implica la selección de la función subyacente (barata) y todos los argumentos predeterminados ( costoso cuando el argumento predeterminado es una list larga de 10M) . Por lo tanto, cada vez que se despacha una tarea en el caso partial , se declina el argumento enlazado, se envía al proceso de trabajo, el proceso de trabajo se despeja y, finalmente, el trabajo “real”. En mi máquina, ese pickle tiene un tamaño aproximado de 50 MB, lo que supone una gran cantidad de gastos generales; en las pruebas de sincronización rápidas en mi máquina, descifrar y descifrar una larga list de 0 millones de tomas toma alrededor de 620 ms (y eso es ignorar la sobrecarga de transferir realmente los 50 MB de datos).

partial deben escabullirse de esta manera, porque no conocen sus propios nombres; cuando elimina una función como f , f (siendo definido) conoce su nombre calificado (en un intérprete interactivo o desde el módulo principal de un progtwig, es __main__.f ), por lo que el lado remoto puede recrearlo localmente haciendo el equivalente a de from __main__ import f . Pero el partial no sabe su nombre; seguro, lo asignó a g , pero ni pickle ni el partial lo saben disponible con el nombre calificado __main__.g ; Podría llamarse foo.fred o un millón de otras cosas. Por lo tanto, tiene que pickle la información necesaria para recrearla completamente desde cero. También es un proceso de pickle para cada llamada (no solo una vez por trabajador) porque no sabe que la llamada no cambia en el elemento principal entre los elementos de trabajo, y siempre trata de asegurarse de que se envíe al estado actual.

Tiene otros problemas (la creación de tiempo de la list solo en el caso partial y la sobrecarga menor de llamar a una función envuelta partial frente a llamar a la función directamente), pero son cambios en relación con la sobrecarga de llamadas por llamada y la eliminación de la partial está agregando (la creación inicial de la list está agregando una sobrecarga única de poco menos de la mitad de lo que cuesta cada ciclo de pickle / unickle; la sobrecarga para llamar a través del partial es menos de un microsegundo).