Bash while read loop es extremadamente lento en comparación con cat, ¿por qué?
Un script de prueba simple aquí:
while read LINE; do
LINECOUNT=$(($LINECOUNT+1))
if [[ $(($LINECOUNT % 1000)) -eq 0 ]]; then echo $LINECOUNT; fi
done
Cuando lo hago, cat my450klinefile.txt | myscript
la CPU se bloquea al 100% y puede procesar alrededor de 1000 líneas por segundo. Unos 5 minutos para procesar lo que cat my450klinefile.txt >/dev/null
se hace en medio segundo.
¿Existe una forma más eficiente de hacer esencialmente esto? Solo necesito leer una línea de la entrada estándar, contar los bytes y escribirla en una canalización con nombre. Pero la velocidad incluso de este ejemplo es increíblemente lenta.
Cada 1 Gb de líneas de entrada necesito realizar algunas acciones de secuencias de comandos más complejas (cerrar y abrir algunas tuberías a las que se alimentan los datos).
La razón while read
es tan lenta es que se requiere que el shell realice una llamada al sistema por cada byte. No puede leer un búfer grande de la tubería, porque el shell no debe leer más de una línea del flujo de entrada y, por lo tanto, debe comparar cada carácter con una nueva línea. Si ejecuta strace
en un while read
bucle, puede ver este comportamiento. Este comportamiento es deseable porque permite hacer de manera confiable cosas como:
while read size; do test "$size" -gt 0 || break; dd bs="$size" count=1 of=file$(( i++ )); done
en el que los comandos dentro del bucle se leen desde la misma secuencia que lee el shell. Si el shell consumiera una gran cantidad de datos leyendo grandes buffers, los comandos internos no tendrían acceso a esos datos. Un efecto secundario desafortunado es que read
es absurdamente lento.
En este caso , es porque el bash
guión se interpreta y no está realmente optimizado para la velocidad. Por lo general, será mejor utilizar una de las herramientas externas, como:
awk 'NR%1000==0{print}' inputFile
que coincide con su muestra de "imprimir cada 1000 líneas".
Si desea (para cada línea) generar el recuento de líneas en caracteres seguidos de la línea misma y canalizarlo a través de otro proceso, también puede hacerlo:
awk '{print length($0)" "$0}' inputFile | someOtherProcess
Herramientas como awk
, sed
, grep
y cut
las más poderosas perl
son mucho más adecuadas para estas tareas que un script de shell interpretado.
La solución Perl para contar bytes de cada cadena:
perl -p -e '
use Encode;
print length(Encode::encode_utf8($_))."\n";$_=""'
Por ejemplo:
dd if=/dev/urandom bs=1M count=100 |
perl -p -e 'use Encode;print length(Encode::encode_utf8($_))."\n";$_=""' |
tail
me funciona como 7.7Mb/s
para comparar la cantidad de script utilizado:
dd if=/dev/urandom bs=1M count=100 >/dev/null
funciona a 9,1 Mb/s
Parece que el guión no es tan lento :)