¿Por qué es más lento el multiproceso Pool que un bucle for?

from multiprocessing import Pool def op1(data): return [data[elem] + 1 for elem in range(len(data))] data = [[elem for elem in range(20)] for elem in range(500000)] import time start_time = time.time() re = [] for data_ in data: re.append(op1(data_)) print('--- %s seconds ---' % (time.time() - start_time)) start_time = time.time() pool = Pool(processes=4) data = pool.map(op1, data) print('--- %s seconds ---' % (time.time() - start_time)) 

Tengo un tiempo de ejecución mucho más lento con el grupo que con el bucle. Pero, ¿no se supone que Pool usa 4 procesadores para hacer el cálculo en paralelo?

Respuesta corta: , las operaciones generalmente se realizarán en (un subconjunto de) los núcleos disponibles. Pero la sobrecarga de comunicación es grande . En su ejemplo, la carga de trabajo es demasiado pequeña en comparación con la sobrecarga .

En caso de que construyas un grupo, se construirán varios trabajadores . Si a continuación, instruye para map entrada dada. Sucede lo siguiente:

  1. los datos se dividirán : cada trabajador recibe una parte aproximadamente justa;
  2. Los datos serán comunicados a los trabajadores;
  3. Cada trabajador procesará su parte del trabajo;
  4. El resultado se comunica de nuevo al proceso . y
  5. El proceso principal agrupa los resultados .

Ahora la división, la comunicación y la unión de datos son todos los procesos que se llevan a cabo mediante el proceso principal. Estos no pueden ser paralelizados . Dado que la operación es rápida ( O (n) con el tamaño de entrada n ), la sobrecarga tiene la misma complejidad de tiempo .

Por lo tanto, incluso si tuviera millones de núcleos, no sería una gran diferencia, ya que la comunicación de la lista es probablemente más costosa que calcular los resultados.

Es por eso que debes paralelizar las tareas costosas computacionalmente . Tareas no sencillas. La cantidad de procesamiento debe ser grande en comparación con la cantidad de comunicación.

En tu ejemplo, el trabajo es trivial : agregas 1 a todos los elementos. Sin embargo, la serialización es menos trivial: tiene que codificar las listas que envía al trabajador.

Hay un par de posibles puntos problemáticos con su código, pero principalmente es demasiado simple.

El módulo de multiprocessing funciona creando diferentes procesos y comunicándose entre ellos. Para cada proceso creado, debe pagar el costo de inicio del proceso del sistema operativo, así como el costo de inicio de python. Esos costos pueden ser altos o bajos, pero en cualquier caso son distintos de cero.

Una vez que pague esos costos de inicio, luego pool.map la función de trabajo en todos los procesos. Lo que básicamente sum 1 a unos pocos números. Esto no es una carga significativa, como demuestran sus pruebas.

Lo que es peor, está utilizando .map() que está ordenado de forma implícita (compare con .imap_unordered() ), por lo que la sincronización está en marcha, lo que deja aún menos libertad para los diversos núcleos de la CPU para darle velocidad.

Si hay un problema aquí, es un problema de “diseño de experimento”: no ha creado un problema suficientemente difícil para que el multiprocessing pueda ayudarlo.

Como han señalado otros, los gastos generales que paga para facilitar el multiprocesamiento son más que los ahorros de tiempo obtenidos al paralelizar a través de múltiples núcleos. En otras palabras, su función op1() no requiere suficientes recursos de CPU para ver la ganancia de rendimiento de la paralelización.

En la clase multiprocessing.Pool , la mayoría de estos escuchados se gastan en serializar y deserializar los datos antes de que los datos se transfieran entre el proceso principal ( que crea el Pool) y los procesos secundarios de “trabajador”.

Esta publicación del blog explora, con mayor detalle, lo costoso que puede ser el pickling ( serialización ) cuando se utiliza el módulo de multiprocessing.Pool .