¿Por qué “sorber” un archivo no es una buena práctica?

¿Por qué “sorber” un archivo no es una buena práctica para la E / S normal de archivos de texto, y cuándo es útil?

Por ejemplo, ¿por qué no debería usar estos?

File.read('/path/to/text.txt').lines.each do |line| # do something with a line end 

o

 File.readlines('/path/to/text.txt').each do |line| # do something with a line end 

Una y otra vez vemos preguntas sobre cómo leer un archivo de texto para procesarlo línea por línea, que usa variaciones de read o líneas de read , que readlines todo el archivo en la memoria en una sola acción.

La documentación para read dice:

Abre el archivo, opcionalmente busca el desplazamiento dado, luego devuelve bytes de longitud (por defecto al rest del archivo). […]

La documentación para readlines dice:

Lee todo el archivo especificado por nombre como líneas individuales, y devuelve esas líneas en una matriz. […]

Insertar un archivo pequeño no es gran cosa, pero llega un punto en el que la memoria debe mezclarse a medida que crece el búfer de los datos entrantes, y eso consume tiempo de CPU. Además, si los datos consumen demasiado espacio, el sistema operativo tiene que involucrarse solo para mantener el script en ejecución y comienza a enviarlo al disco, lo que hará que un progtwig se ponga de rodillas. En un HTTPd (host web) o algo que necesite una respuesta rápida, paralizará toda la aplicación.

Slurping generalmente se basa en un malentendido de la velocidad de E / S de archivos o pensando que es mejor leer y dividir el búfer de lo que es leerlo una sola línea a la vez.

Aquí hay algunos códigos de prueba para demostrar el problema causado por “sorber”.

Guarde esto como “test.sh”:

 echo Building test files... yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000 > kb.txt yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000000 > mb.txt yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000000000 > gb1.txt cat gb1.txt gb1.txt > gb2.txt cat gb1.txt gb2.txt > gb3.txt echo Testing... ruby -v echo for i in kb.txt mb.txt gb1.txt gb2.txt gb3.txt do echo echo "Running: time ruby readlines.rb $i" time ruby readlines.rb $i echo '---------------------------------------' echo "Running: time ruby foreach.rb $i" time ruby foreach.rb $i echo done rm [km]b.txt gb[123].txt 

Crea cinco archivos de tamaños crecientes. Los archivos 1K se procesan fácilmente y son muy comunes. Solía ​​ser que los archivos de 1MB se consideraban grandes, pero ahora son comunes. 1 GB es común en mi entorno, y los archivos de más de 10 GB se encuentran periódicamente, por lo que es muy importante saber qué sucede a 1 GB y más allá.

Guarde esto como “readlines.rb”. No hace más que leer el archivo completo línea por línea internamente, y anexarlo a una matriz que luego se devuelve, y parece que sería rápido ya que está escrito en C:

 lines = File.readlines(ARGV.shift).size puts "#{ lines } lines read" 

Guarde esto como “foreach.rb”:

 lines = 0 File.foreach(ARGV.shift) { |l| lines += 1 } puts "#{ lines } lines read" 

Ejecutando sh ./test.sh en mi computadora portátil obtengo:

 Building test files... Testing... ruby 2.1.2p95 (2014-05-08 revision 45877) [x86_64-darwin13.0] 

Leyendo el archivo 1K:

 Running: time ruby readlines.rb kb.txt 28 lines read real 0m0.998s user 0m0.386s sys 0m0.594s --------------------------------------- Running: time ruby foreach.rb kb.txt 28 lines read real 0m1.019s user 0m0.395s sys 0m0.616s 

Leyendo el archivo de 1MB:

 Running: time ruby readlines.rb mb.txt 27028 lines read real 0m1.021s user 0m0.398s sys 0m0.611s --------------------------------------- Running: time ruby foreach.rb mb.txt 27028 lines read real 0m0.990s user 0m0.391s sys 0m0.591s 

Leyendo el archivo de 1GB:

 Running: time ruby readlines.rb gb1.txt 27027028 lines read real 0m19.407s user 0m17.134s sys 0m2.262s --------------------------------------- Running: time ruby foreach.rb gb1.txt 27027028 lines read real 0m10.378s user 0m9.472s sys 0m0.898s 

Leyendo el archivo de 2GB:

 Running: time ruby readlines.rb gb2.txt 54054055 lines read real 0m58.904s user 0m54.718s sys 0m4.029s --------------------------------------- Running: time ruby foreach.rb gb2.txt 54054055 lines read real 0m19.992s user 0m18.765s sys 0m1.194s 

Leyendo el archivo de 3GB:

 Running: time ruby readlines.rb gb3.txt 81081082 lines read real 2m7.260s user 1m57.410s sys 0m7.007s --------------------------------------- Running: time ruby foreach.rb gb3.txt 81081082 lines read real 0m33.116s user 0m30.790s sys 0m2.134s 

Observe cómo las readlines lectura se ralentizan dos veces cada vez que aumenta el tamaño del archivo, y el uso de foreach ralentiza linealmente. En 1MB, podemos ver que hay algo que afecta la E / S “sorber” que no afecta la lectura línea por línea. Y, debido a que los archivos de 1MB son muy comunes en estos días, es fácil ver que ralentizarán el procesamiento de los archivos a lo largo de la vida útil de un progtwig si no pensamos en el futuro. Un par de segundos aquí o allá no son mucho cuando suceden una vez, pero si ocurren varias veces por minuto, sumn un grave impacto en el rendimiento antes de fin de año.

Me encontré con este problema hace años cuando procesaba archivos de datos grandes. El código de Perl que estaba usando se detendría periódicamente ya que reasignó la memoria al cargar el archivo. Volver a escribir el código para no sorber el archivo de datos, y en su lugar leerlo y procesarlo línea por línea, dio una gran mejora de velocidad de más de cinco minutos para ejecutar a menos de uno y me dio una gran lección.

“sorber” un archivo a veces es útil, especialmente si tiene que hacer algo más allá de los límites de las líneas, sin embargo, vale la pena pasar un tiempo pensando en formas alternativas de leer un archivo si tiene que hacer eso. Por ejemplo, considere mantener un pequeño buffer creado a partir de las últimas “n” líneas y escanearlo. Eso evitará problemas de administración de memoria causados ​​por tratar de leer y mantener todo el archivo. Esto se discute en un blog relacionado con Perl ” Perl Slurp-Eaze ” que cubre los “whens” y “whys” para justificar el uso de lecturas completas de archivos, y se aplica bien a Ruby.

Por otras excelentes razones para no “sorber” sus archivos, lea ” Cómo buscar texto de archivo para un patrón y reemplácelo con un valor determinado “.

¿Por qué “sorber” un archivo no es una buena práctica para la E / S normal de archivos de texto?

The Tin Man golpea bien. También me gustaría agregar:

  • En muchos casos, leer el archivo completo en la memoria no es manejable (porque el archivo es demasiado grande o las manipulaciones de cadena tienen un espacio O () exponencial)

  • Muchas veces, no se puede anticipar el tamaño del archivo (caso especial de arriba)

  • Siempre debe tratar de ser consciente del uso de la memoria, y leer todo el archivo de una vez (incluso en situaciones triviales) no es una buena práctica si existe una opción alternativa (por ejemplo, línea por línea). Sé por experiencia que VBS es horrible en este sentido y uno se ve obligado a manipular archivos a través de la línea de comandos.

Este concepto se aplica no solo a los archivos, sino a cualquier otro proceso donde el tamaño de su memoria crece rápidamente y debe manejar cada iteración (o línea) a la vez. Las funciones del generador lo ayudan manejando el proceso, o la lectura de línea, uno por uno para no trabajar con todos los datos en la memoria.

Como un lado / extra, Python es muy inteligente en la lectura de archivos y su método open() está diseñado para leer línea por línea de forma predeterminada. Consulte ” Mejorar su Python: rendimiento y Generadores explicados “, que explica un buen ejemplo de caso de uso para las funciones del generador.