El uso de multiprocessing.Manager.list en lugar de una lista real hace que el cálculo lleve años

Quería probar diferentes formas de usar multiprocessing comenzando con este ejemplo:

 $ cat multi_bad.py import multiprocessing as mp from time import sleep from random import randint def f(l, t): # sleep(30) return sum(x < t for x in l) if __name__ == '__main__': l = [randint(1, 1000) for _ in range(25000)] t = [randint(1, 1000) for _ in range(4)] # sleep(15) pool = mp.Pool(processes=4) result = pool.starmap_async(f, [(l, x) for x in t]) print(result.get()) 

Aquí, l es una lista que se copia 4 veces cuando se generan 4 procesos. Para evitar eso, la página de documentación ofrece el uso de colas, matrices compartidas u objetos proxy creados mediante el multiprocessing.Manager . Para el último, cambié la definición de l :

 $ diff multi_bad.py multi_good.py 10c10,11  man = mp.Manager() > l = man.list([randint(1, 1000) for _ in range(25000)]) 

Los resultados siguen siendo correctos, pero el tiempo de ejecución ha aumentado tan drásticamente que creo que estoy haciendo algo mal:

 $ time python multi_bad.py [17867, 11103, 2021, 17918] real 0m0.247s user 0m0.183s sys 0m0.010s $ time python multi_good.py [3609, 20277, 7799, 24262] real 0m15.108s user 0m28.092s sys 0m6.320s 

Los documentos dicen que esta forma es más lenta que las matrices compartidas, pero esto simplemente se siente mal. Tampoco estoy seguro de cómo puedo hacer un perfil de esto para obtener más información sobre lo que está sucediendo. ¿Me estoy perdiendo de algo?

PS Con matrices compartidas obtengo tiempos por debajo de 0.25s.

PPS Esto está en Linux y Python 3.3.

Linux usa la copia en escritura cuando los subprocesos son os.fork ed. Demostrar:

 import multiprocessing as mp import numpy as np import logging import os logger = mp.log_to_stderr(logging.WARNING) def free_memory(): total = 0 with open('/proc/meminfo', 'r') as f: for line in f: line = line.strip() if any(line.startswith(field) for field in ('MemFree', 'Buffers', 'Cached')): field, amount, unit = line.split() amount = int(amount) if unit != 'kB': raise ValueError( 'Unknown unit {u!r} in /proc/meminfo'.format(u = unit)) total += amount return total def worker(i): x = data[i,:].sum() # Exercise access to data logger.warn('Free memory: {m}'.format(m = free_memory())) def main(): procs = [mp.Process(target = worker, args = (i, )) for i in range(4)] for proc in procs: proc.start() for proc in procs: proc.join() logger.warn('Initial free: {m}'.format(m = free_memory())) N = 15000 data = np.ones((N,N)) logger.warn('After allocating data: {m}'.format(m = free_memory())) if __name__ == '__main__': main() 

que rindió

 [WARNING/MainProcess] Initial free: 2522340 [WARNING/MainProcess] After allocating data: 763248 [WARNING/Process-1] Free memory: 760852 [WARNING/Process-2] Free memory: 757652 [WARNING/Process-3] Free memory: 757264 [WARNING/Process-4] Free memory: 756760 

Esto muestra que inicialmente había aproximadamente 2.5GB de memoria libre. Después de asignar una matriz de float64 de float64 s, había 763248 KB libres. Esto tiene sentido desde 15000 ** 2 * 8 bytes = 1.8GB y la caída en la memoria, 2.5GB – 0.763248GB también es aproximadamente 1.8GB.

Ahora, después de que se genera cada proceso, se reporta que la memoria libre es ~ 750MB. No hay una disminución significativa en la memoria libre, por lo que concluyo que el sistema debe estar utilizando la función de copia en escritura.

Conclusión: si no necesita modificar los datos, definirlos a nivel global del módulo __main__ es una forma conveniente y (al menos en Linux) de compartir datos entre subprocesos.

Esto es de esperar porque acceder a objetos compartidos significa tener que encurtir la solicitud, enviarla a través de algún tipo de señal / syscall, eliminar la solicitud, realizarla y devolver el resultado de la misma manera.

Básicamente, debes tratar de evitar compartir la memoria tanto como puedas. Esto lleva a un código más depurable (porque tiene mucha menos concurrencia) y la velocidad es mayor.

La memoria compartida solo debe usarse si es realmente necesaria (por ejemplo, compartir gigabytes de datos para que copiarlos requiera demasiada memoria RAM o si los procesos deberían poder interactuar a través de esta memoria compartida).

En una nota al margen, probablemente el uso del Administrador sea mucho más lento que un Array compartido porque el Administrador debe ser capaz de manejar cualquier PyObject * y, por lo tanto, tiene que escabullirse / quitarse las manchas, mientras que los arreglos pueden evitar gran parte de esta sobrecarga.

De la documentación del multiprocesamiento:

Los administradores proporcionan una forma de crear datos que pueden compartirse entre diferentes procesos. Un objeto administrador controla un proceso de servidor que administra objetos compartidos. Otros procesos pueden acceder a los objetos compartidos utilizando proxies.

Por lo tanto, usar un Administrador significa generar un nuevo proceso que se usa solo para manejar la memoria compartida, por eso es probable que tome mucho más tiempo.

Si intentas perfilar la velocidad del proxy, es mucho más lento que una lista no compartida:

 >>> import timeit >>> import multiprocessing as mp >>> man = mp.Manager() >>> L = man.list(range(25000)) >>> timeit.timeit('L[0]', 'from __main__ import L') 50.490395069122314 >>> L = list(range(25000)) >>> timeit.timeit('L[0]', 'from __main__ import L') 0.03588080406188965 >>> 50.490395069122314 / _ 1407.1701119638526 

Mientras que un Array no es mucho más lento:

 >>> L = mp.Array('i', range(25000)) >>> timeit.timeit('L[0]', 'from __main__ import L') 0.6133401393890381 >>> 0.6133401393890381 / 0.03588080406188965 17.09382371507359 

Dado que las operaciones muy elementales son lentas y no creo que haya muchas esperanzas de acelerarlas, esto significa que si tiene que compartir una gran lista de datos y desea un acceso rápido, entonces debe usar una Array .

Algo que podría acelerar un poco las cosas es acceder a más de un elemento a la vez (p. Ej., Obtener cortes en lugar de elementos individuales), pero dependiendo de lo que quiera hacer esto puede o no ser posible.