Concurrencia / Paralelismo en Windows con Python

Desarrollé un progtwig simple para resolver el problema de ocho reinas. Ahora me gustaría hacer más pruebas con diferentes meta-parámetros, así que me gustaría hacerlo rápido. Pasé por varias iteraciones de creación de perfiles y pude reducir significativamente el tiempo de ejecución, pero llegué al punto en el que creo que solo partes de los cálculos al mismo tiempo podrían hacerlo más rápido. Intenté usar módulos de multiprocessing y concurrent.futures Pero no mejoró mucho el tiempo de ejecución y, en algunos casos, incluso ralentizó la ejecución. Eso es solo para dar un poco de contexto.

Pude encontrar una estructura de código similar en la que la versión secuencial supere al mismo tiempo.

 import numpy as np import concurrent.futures import math import time import multiprocessing def is_prime(n): if n % 2 == 0: return False sqrt_n = int(math.floor(math.sqrt(n))) for i in range(3, sqrt_n + 1, 2): if n % i == 0: return False return True def generate_data(seed): np.random.seed(seed) numbers = [] for _ in range(5000): nbr = np.random.randint(50000, 100000) numbers.append(nbr) return numbers def run_test_concurrent(numbers): print("Concurrent test") start_tm = time.time() chunk = len(numbers)//3 primes = None with concurrent.futures.ProcessPoolExecutor(max_workers=3) as pool: primes = list(pool.map(is_prime, numbers, chunksize=chunk)) print("Time: {:.6f}".format(time.time() - start_tm)) print("Number of primes: {}\n".format(np.sum(primes))) def run_test_sequential(numbers): print("Sequential test") start_tm = time.time() primes = [is_prime(nbr) for nbr in numbers] print("Time: {:.6f}".format(time.time() - start_tm)) print("Number of primes: {}\n".format(np.sum(primes))) def run_test_multiprocessing(numbers): print("Multiprocessing test") start_tm = time.time() chunk = len(numbers)//3 primes = None with multiprocessing.Pool(processes=3) as pool: primes = list(pool.map(is_prime, numbers, chunksize=chunk)) print("Time: {:.6f}".format(time.time() - start_tm)) print("Number of primes: {}\n".format(np.sum(primes))) def main(): nbr_trails = 5 for trail in range(nbr_trails): numbers = generate_data(trail*10) run_test_concurrent(numbers) run_test_sequential(numbers) run_test_multiprocessing(numbers) print("--\n") if __name__ == '__main__': main() 

Cuando lo ejecuté en mi máquina: Windows 7, Intel Core i5 con cuatro núcleos obtuve el siguiente resultado:

 Concurrent test Time: 2.006006 Number of primes: 431 Sequential test Time: 0.010000 Number of primes: 431 Multiprocessing test Time: 1.412003 Number of primes: 431 -- Concurrent test Time: 1.302003 Number of primes: 447 Sequential test Time: 0.010000 Number of primes: 447 Multiprocessing test Time: 1.252003 Number of primes: 447 -- Concurrent test Time: 1.280002 Number of primes: 446 Sequential test Time: 0.010000 Number of primes: 446 Multiprocessing test Time: 1.250002 Number of primes: 446 -- Concurrent test Time: 1.260002 Number of primes: 446 Sequential test Time: 0.010000 Number of primes: 446 Multiprocessing test Time: 1.250002 Number of primes: 446 -- Concurrent test Time: 1.282003 Number of primes: 473 Sequential test Time: 0.010000 Number of primes: 473 Multiprocessing test Time: 1.260002 Number of primes: 473 -- 

La pregunta que tengo es si puedo hacerlo más rápido ejecutándolo simultáneamente en Windows con Python 3.6.4 |Anaconda, Inc.| . Leí aquí en SO ( ¿Por qué es más caro crear un nuevo proceso en Windows que en Linux? ) Que crear nuevos procesos en Windows es costoso. ¿Hay algo que se pueda hacer para acelerar las cosas? ¿Me estoy perdiendo algo obvio?

También intenté crear Pool solo una vez, pero no pareció ayudar mucho.


Editar:

La estructura del código original se parece más o menos a:

Mi código es la estructura más o menos así:

 class Foo(object): def g() -> int: # function performing simple calculations # single function call is fast (~500 ms) pass def run(self): nbr_processes = multiprocessing.cpu_count() - 1 with multiprocessing.Pool(processes=nbr_processes) as pool: foos = get_initial_foos() solution_found = False while not solution_found: # one iteration chunk = len(foos)//nbr_processes vals = list(pool.map(Foo.g, foos, chunksize=chunk)) foos = modify_foos() 

con foos tienen 1000 elementos. No es posible saber de antemano qué tan rápido convergen los algoritmos y cuántas iteraciones se ejecutan, posiblemente miles.

Su configuración no es realmente justa para multiprocesamiento. Incluso incluiste primes = None innecesarios primes = None asignación. 😉

Algunos puntos:


Tamaño de datos

Sus datos generados son una forma de aclararse para permitir recuperar la sobrecarga de la creación del proceso. Pruebe con el range(1_000_000) lugar del range(5000) . En Linux con multiprocessing.start_method establecido en ‘spawn’ (predeterminado en Windows), esto dibuja una imagen diferente:

 Concurrent test Time: 0.957883 Number of primes: 89479 Sequential test Time: 1.235785 Number of primes: 89479 Multiprocessing test Time: 0.714775 Number of primes: 89479 

Reutilizar su piscina

No deje el bloque de bloque con el bloque siempre que haya dejado en el progtwig el código que desea paralelizar más tarde. Si crea la agrupación solo una vez al principio, no tiene mucho sentido incluir la creación de la agrupación en su punto de referencia.


Numpy

Numpy está en partes capaz de liberar el locking global del intérprete ( GIL ). Esto significa que puede beneficiarse del paralelismo de múltiples núcleos sin la sobrecarga de la creación de procesos. Si estás haciendo matemáticas de todos modos, trata de utilizar el número lo más posible. Pruebe concurrent.futures.ThreadPoolExecutor y multiprocessing.dummy.Pool con código usando numpy.

Los procesos son mucho más ligeros en las variantes UNIX. Los procesos de Windows son pesados ​​y tardan mucho más tiempo en iniciarse. Los hilos son la forma recomendada de hacer multiprocesamiento en ventanas. También puede seguir este hilo: ¿Por qué crear un nuevo proceso es más caro en Windows que en Linux?