Leer archivo grande en paralelo?

Tengo un archivo grande que necesito leer y crear un diccionario. Me gustaría que esto fuera lo más rápido posible. Sin embargo, mi código en python es demasiado lento. Aquí hay un ejemplo mínimo que muestra el problema.

Primero haz algunos datos falsos

paste <(seq 20000000)  largefile.txt 

Ahora aquí hay una pieza mínima de código python para leerlo y hacer un diccionario.

 import sys from collections import defaultdict fin = open(sys.argv[1]) dict = defaultdict(list) for line in fin: parts = line.split() dict[parts[0]].append(parts[1]) 

Tiempos:

 time ./read.py largefile.txt real 0m55.746s 

Sin embargo, es posible leer todo el archivo mucho más rápido como:

 time cut -f1 largefile.txt > /dev/null real 0m1.702s 

Mi CPU tiene 8 núcleos, ¿es posible paralelizar este progtwig en python para acelerarlo?

Una posibilidad podría ser leer en grandes partes de la entrada y luego ejecutar 8 procesos en paralelo en diferentes subunidades no superpuestas, haciendo diccionarios en paralelo desde los datos en la memoria y luego leer en otra parte grande. ¿Es esto posible en Python utilizando multiprocesamiento de alguna manera?

Actualizar Los datos falsos no eran muy buenos ya que solo tenían un valor por clave. Mejor es

 perl -E 'say int rand 1e7, $", int rand 1e4 for 1 .. 1e7' > largefile.txt 

(Relacionado con Leer en archivo grande y hacer diccionario .)

Hace varios años hubo una serie de publicaciones en el blog “Wide Finder Project” sobre esto en el sitio de Tim Bray [1]. Puede encontrar una solución [2] por Fredrik Lundh de ElementTree [3] y PIL [4] fame. Sé que la publicación de enlaces está generalmente desaconsejada en este sitio, pero creo que estos enlaces le dan una mejor respuesta que copiar y pegar su código.

[1] http://www.tbray.org/ongoing/When/200x/2007/10/30/WF-Results
[2] http://effbot.org/zone/wide-finder.htm
[3] http://docs.python.org/3/library/xml.etree.elementtree.html
[4] http://www.pythonware.com/products/pil/

Puede ser posible paralelizar esto para acelerarlo, pero es poco probable que la realización de múltiples lecturas en paralelo ayude.

Es improbable que su sistema operativo realice múltiples lecturas en paralelo (la excepción es algo así como una matriz de incursión a rayas, en cuyo caso aún necesita conocer el paso para hacer un uso óptimo de la misma).

Lo que puede hacer es ejecutar las operaciones de cadena / diccionario / lista relativamente caras en paralelo a la lectura.

Por lo tanto, un hilo lee y empuja trozos (grandes) a una cola sincronizada, uno o más subprocesos de consumidores extraen trozos de la cola, los dividen en líneas y rellenan el diccionario.

(Si elige varios subprocesos de consumo, como dice Pappnese, cree un diccionario por subproceso y luego únase a ellos).


Consejos:

  • … empuja los trozos a una cola sincronizada …
  • … uno o más hilos de consumo …

Re. generosidad:

C, obviamente, no tiene la GIL con la que lidiar, por lo que es probable que varios consumidores aumenten de escala. Sin embargo, el comportamiento de lectura no cambia. La desventaja es que C carece de soporte incorporado para los mapas hash (asumiendo que todavía quieres un diccionario al estilo de Python) y colas sincronizadas, por lo que debes encontrar los componentes adecuados o escribir el tuyo. La estrategia básica de múltiples consumidores, cada uno de los cuales construye su propio diccionario y luego los fusiona al final, es probablemente la mejor.

El uso de strtok_r lugar de str.split puede ser más rápido, pero recuerde que también necesitará administrar la memoria para todas sus cadenas manualmente. Ah, y necesitas lógica para gestionar los fragmentos de línea también. Honestamente C te ofrece tantas opciones que creo que solo necesitarás crear un perfil y verlas.

Parece tentador pensar que usar un grupo de procesamiento solucionará problemas como este, pero va a ser un poco más complicado que eso, al menos en Python puro.

Debido a que el OP mencionó que las listas en cada línea de entrada serían más largas en la práctica que dos elementos, hice un archivo de entrada un poco más realista usando:

 paste <(seq 20000000) <(seq 2 20000001) <(seq 3 20000002) | head -1000000 > largefile.txt 

Después de perfilar el código original, encontré que la parte más lenta del proceso era la rutina de división de líneas. ( .split() tardó aproximadamente 2 .append() más que .append() en mi máquina).

 1000000 0.333 0.000 0.333 0.000 {method 'split' of 'str' objects} 1000000 0.154 0.000 0.154 0.000 {method 'append' of 'list' objects} 

Así que factoré la división en otra función y uso un grupo para distribuir el trabajo de dividir los campos:

 import sys import collections import multiprocessing as mp d = collections.defaultdict(list) def split(l): return l.split() pool = mp.Pool(processes=4) for keys in pool.map(split, open(sys.argv[1])): d[keys[0]].append(keys[1:]) 

Desafortunadamente, agregar la piscina ralentizó las cosas en más de 2x. La versión original se veía así:

 $ time python process.py smallfile.txt real 0m7.170s user 0m6.884s sys 0m0.260s 

frente a la versión paralela:

 $ time python process-mp.py smallfile.txt real 0m16.655s user 0m24.688s sys 0m1.380s 

Debido a que la llamada .map() básicamente tiene que serializar (pickle) cada entrada, enviarla al proceso remoto, y luego deserializar (eliminar) el valor de retorno del proceso remoto, usar una agrupación de esta manera es mucho más lento. Obtienes alguna mejora al agregar más núcleos al grupo, pero diría que esta es fundamentalmente la forma incorrecta de distribuir este trabajo.

Para realmente acelerar esto a través de los núcleos, mi conjetura es que necesitarías leer grandes porciones de la entrada usando algún tipo de tamaño de bloque fijo. Luego, puede enviar todo el bloque a un proceso de trabajo y obtener listas en serie de nuevo (aunque aún se desconoce cuánto le costará la deserialización aquí). Sin embargo, leer la entrada en bloques de tamaño fijo puede parecer complicado con la entrada anticipada, ya que supongo que cada línea no tiene necesariamente la misma longitud.

Una cosa que podría intentar es obtener el recuento de líneas del archivo, luego generar 8 hilos que forman un diccionario de 1/8 del archivo cada uno, luego unirse a los diccionarios cuando todos los hilos estén terminados. Esto probablemente lo acelerará si es el agregado lo que toma tiempo y no la lectura de las líneas.

Más solución cardinal para la adición lenta de diccionarios: reemplace el diccionario con un conjunto de pares de cadenas. Llénalo y luego ordénalo.

Si sus datos en el archivo no cambian con tanta frecuencia, puede optar por serializarlos. El intérprete de Python lo deserializará mucho más rápido. Puede utilizar el módulo cPickle.

O crear 8 procesos separados es otra opción. Porque, tener un único dictado lo hace mucho más posible. Puede interactuar entre esos procesos a través de Pipe en el módulo “multiprocesamiento” o en el módulo “socket”.

Atentamente

Barış ÇUHADAR.