¿Por qué hay tanta diferencia en el tiempo de ejecución de echo y cat?


15

Responder esta pregunta me hizo hacer otra pregunta:
pensé que los siguientes scripts hacen lo mismo y el segundo debería ser mucho más rápido, porque el primero usa cateso para abrir el archivo una y otra vez, pero el segundo solo abre el archivo una vez y luego solo hace eco de una variable:

(Consulte la sección de actualización para obtener el código correcto).

Primero:

#!/bin/sh
for j in seq 10; do
  cat input
done >> output

Segundo:

#!/bin/sh
i=`cat input`
for j in seq 10; do
  echo $i
done >> output

mientras que la entrada es de unos 50 megabytes.

Pero cuando probé el segundo, fue demasiado lento porque hacer eco de la variable ifue un proceso masivo. También tuve algunos problemas con el segundo script, por ejemplo, el tamaño del archivo de salida fue menor de lo esperado.

También revisé la página del manual echoy las catcomparé:

echo: muestra una línea de texto

cat: concatena archivos e imprime en la salida estándar

Pero no entendí la diferencia.

Entonces:

  • ¿Por qué el gato es tan rápido y el eco tan lento en el segundo guión?
  • ¿O es el problema con variable i? (porque en la página del manual echose dice que muestra "una línea de texto" y supongo que está optimizado solo para variables cortas, no para variables muy largas como i. Sin embargo, eso es solo una suposición).
  • ¿Y por qué tengo problemas cuando lo uso echo?

ACTUALIZAR

Utilicé en seq 10lugar de `seq 10`incorrectamente. Este es el código editado:

Primero:

#!/bin/sh
for j in `seq 10`; do
  cat input
done >> output

Segundo:

#!/bin/sh
i=`cat input`
for j in `seq 10`; do
  echo $i
done >> output

(Un agradecimiento especial a roaima ).

Sin embargo, no es el punto del problema. Incluso si el bucle ocurre solo una vez, me sale el mismo problema: catfunciona mucho más rápido que echo.


1
y ¿qué pasa cat $(for i in $(seq 1 10); do echo "input"; done) >> output? :)
netmonk

2
El echoes más rápido. Lo que te falta es que estás haciendo que el shell haga demasiado trabajo al no citar las variables cuando las usas.
roaima

Citar las variables no es el problema; El problema es la variable i misma (es decir, usarla como un paso intermedio entre la entrada y la salida).
Aleksander

`echo $ i` - no hagas esto. Use printf y cite el argumento.
PSkocik

1
@PSkocik Lo que digo es que quieres printf '%s' "$i", no echo $i. @cuonglm explica bien algunos de los problemas del eco en su respuesta. Para saber por qué incluso las citas no son suficientes en algunos casos con eco, consulte unix.stackexchange.com/questions/65803/…
PSkocik

Respuestas:


24

Hay varias cosas a considerar aquí.

i=`cat input`

puede ser costoso y hay muchas variaciones entre conchas.

Esa es una característica llamada sustitución de comando. La idea es almacenar toda la salida del comando menos los caracteres de nueva línea finales en la ivariable en la memoria.

Para hacer eso, los shells bifurcan el comando en un subshell y leen su salida a través de una tubería o par de conectores. Ves mucha variación aquí. En un archivo de 50MiB aquí, puedo ver, por ejemplo, que bash es 6 veces más lento que ksh93 pero un poco más rápido que zsh y dos veces más rápido que yash.

La razón principal de bashser lento es que lee 128 bytes de la tubería a la vez (mientras que otros shells leen 4KiB u 8KiB a la vez) y está penalizado por la sobrecarga de llamadas del sistema.

zshnecesita realizar un procesamiento posterior para escapar de los bytes NUL (otros shells se rompen en los bytes NUL), y yashrealiza un procesamiento aún más pesado al analizar caracteres de varios bytes.

Todos los shells necesitan quitar los caracteres de línea nueva que pueden estar haciendo de manera más o menos eficiente.

Algunos pueden querer manejar bytes NUL con más gracia que otros y verificar su presencia.

Luego, una vez que tenga esa gran variable en la memoria, cualquier manipulación implica generalmente asignar más memoria y hacer frente a los datos.

Aquí, está pasando (tenía la intención de pasar) el contenido de la variable a echo.

Afortunadamente, echoestá integrado en su shell, de lo contrario, la ejecución probablemente habría fallado con un error de lista arg demasiado larga . Incluso entonces, construir la matriz de la lista de argumentos posiblemente implicará copiar el contenido de la variable.

El otro problema principal en su enfoque de sustitución de comandos es que está invocando el operador split + glob (olvidando citar la variable).

Para eso, los shells deben tratar la cadena como una cadena de caracteres (aunque algunos shells no lo hacen y son defectuosos en ese sentido), por lo que en las configuraciones regionales UTF-8, eso significa analizar secuencias UTF-8 (si no se ha hecho ya yash) , busque $IFScaracteres en la cadena. Si $IFScontiene espacio, tabulación o nueva línea (que es el caso por defecto), el algoritmo es aún más complejo y costoso. Luego, las palabras resultantes de esa división deben asignarse y copiarse.

La parte glob será aún más cara. Si cualquiera de esas palabras contienen caracteres glob ( *, ?, [), entonces la cáscara tendrá que leer el contenido de algunos directorios y hacer un poco caro coincidencia de patrones ( bash's aplicación, por ejemplo, es notoriamente muy mala, por cierto).

Si la entrada contiene algo como /*/*/*/../../../*/*/*/../../../*/*/*eso, será extremadamente costoso ya que eso significa listar miles de directorios y eso puede expandirse a varios cientos de MiB.

Luego echo, normalmente hará un procesamiento adicional. Algunas implementaciones expanden \xsecuencias en el argumento que recibe, lo que significa analizar el contenido y probablemente otra asignación y copia de los datos.

Por otro lado, OK, en la mayoría de los shells catno está integrado, lo que significa bifurcar un proceso y ejecutarlo (cargar el código y las bibliotecas), pero después de la primera invocación, ese código y el contenido del archivo de entrada será almacenado en la memoria caché. Por otro lado, no habrá intermediario. catleerá grandes cantidades a la vez y lo escribirá de inmediato sin procesar, y no necesita asignar una gran cantidad de memoria, solo ese búfer que reutiliza.

También significa que es mucho más confiable, ya que no se ahoga en bytes NUL y no recorta los caracteres de línea nueva (y no divide + glob, aunque puede evitarlo citando la variable, y no expanda la secuencia de escape, aunque puede evitarlo usando en printflugar de echo).

Si desea optimizarlo aún más, en lugar de invocar catvarias veces, simplemente pase inputvarias veces a cat.

yes input | head -n 100 | xargs cat

Ejecutará 3 comandos en lugar de 100.

Para hacer que la versión variable sea más confiable, necesitaría usar zsh(otros shells no pueden hacer frente a bytes NUL) y hacerlo:

zmodload zsh/mapfile
var=$mapfile[input]
repeat 10 print -rn -- "$var"

Si sabe que la entrada no contiene bytes NUL, entonces puede hacerlo de manera confiable POSIXY (aunque puede que no funcione donde printfno está integrado) con:

i=$(cat input && echo .) || exit # add an extra .\n to avoid trimming newlines
i=${i%.} # remove that trailing dot (the \n was removed by cmdsubst)
n=10
while [ "$n" -gt 10 ]; do
  printf %s "$i"
  n=$((n - 1))
done

Pero eso nunca será más eficiente que usarlo caten el bucle (a menos que la entrada sea muy pequeña).


Vale la pena mencionar que en caso de una larga discusión, puede salir de la memoria . Ejemplo/bin/echo $(perl -e 'print "A"x999999')
cuonglm

Estás equivocado con la suposición de que el tamaño de lectura tiene una influencia significativa, así que lee mi respuesta para entender la verdadera razón.
schily

@schily, realizar 409600 lecturas de 128 bytes lleva más tiempo (tiempo del sistema) que 800 lecturas de 64k. Comparar dd bs=128 < input > /dev/nullcon dd bs=64 < input > /dev/null. De los 0.6s que toma bash para leer ese archivo, 0.4 se gastan en esas readllamadas al sistema en mis pruebas, mientras que otros shells pasan mucho menos tiempo allí.
Stéphane Chazelas

Bueno, no parece haber ejecutado un análisis de rendimiento real. La influencia de la llamada de lectura (al comparar diferentes tamaños de lectura) es de aprox. 1% de todo el tiempo, mientras que las funciones readwc() y trim()en Burne Shell ocupan el 30% de todo el tiempo y esto probablemente se subestima, ya que no hay una biblioteca con gprofanotaciones mbtowc().
schily

¿A cuál se \xexpande?
Mohammad

11

El problema no se trata caty echose trata de la variable de cotización olvidada $i.

En el script de shell similar a Bourne (excepto zsh), dejar variables entre comillas causa glob+splitoperadores en las variables.

$var

es en realidad:

glob(split($var))

Por lo tanto, con cada iteración de bucle, todo el contenido de input(excluir nuevas líneas finales) se expandirá, dividirá, englobará. Todo el proceso requiere shell para asignar memoria, analizando la cadena una y otra vez. Esa es la razón por la que obtuviste el mal desempeño.

Puede citar la variable para prevenir, glob+splitpero no le ayudará mucho, ya que cuando el shell todavía necesita construir el argumento de cadena grande y escanear su contenido echo(Reemplazarlo echopor externo /bin/echole dará la lista de argumentos demasiado tiempo o sin memoria) Depende del $itamaño). La mayor parte de la echoimplementación no es compatible con POSIX, expandirá las \xsecuencias de barra invertida en los argumentos que recibió.

Con cat, el shell solo necesita generar un proceso en cada iteración de bucle y catrealizará la copia de E / S. El sistema también puede almacenar en caché el contenido del archivo para acelerar el proceso cat.


2
@roaima: No mencionaste la parte glob, que puede ser una gran razón, al imaginar algo que /*/*/*/*../../../../*/*/*/*/../../../../puede estar en el contenido del archivo. Solo quiero señalar los detalles .
Cuonglm

Te tengo que agradecer. Incluso sin eso, el tiempo se duplica cuando se usa una variable sin comillas
roaima

1
time echo $( <xdditg106) >/dev/null real 0m0.125s user 0m0.085s sys 0m0.025s time echo "$( <xdditg106)" >/dev/null real 0m0.047s user 0m0.016s sys 0m0.022s
netmonk

No entendí por qué las citas no pueden resolver el problema. Necesito mas descripcion.
Mohammad

1
@ mohammad.k: Como escribí en mi respuesta, la variable de cotización previene glob+splitparte, y acelerará el ciclo while. Y también noté que no te ayudará mucho. Desde cuando la mayoría del echocomportamiento del shell no es compatible con POSIX. printf '%s' "$i"es mejor.
Cuonglm

2

Si llamas

i=`cat input`

esto permite que su proceso de shell crezca de 50 MB a 200 MB (dependiendo de la implementación interna de caracteres anchos) Esto puede hacer que su shell sea lento, pero este no es el problema principal.

El problema principal es que el comando anterior necesita leer todo el archivo en la memoria de shell y echo $idebe dividir los campos en el contenido del archivo $i. Para dividir los campos, todo el texto del archivo debe convertirse en caracteres anchos y aquí es donde se pasa la mayor parte del tiempo.

Hice algunas pruebas con el caso lento y obtuve estos resultados:

  • El más rápido es ksh93
  • El siguiente es mi Bourne Shell (2 veces más lento que ksh93)
  • El siguiente es bash (3 veces más lento que ksh93)
  • El último es ksh88 (7 veces más lento que ksh93)

La razón por la cual ksh93 es el más rápido parece ser que ksh93 no usa mbtowc()desde libc sino más bien una implementación propia.

Por cierto: Stephane se equivoca de que el tamaño de lectura tiene cierta influencia, compilé el Bourne Shell para leer en fragmentos de 4096 bytes en lugar de 128 bytes y obtuve el mismo rendimiento en ambos casos.


El i=`cat input`comando no divide los campos, es el echo $ique lo hace. El tiempo dedicado i=`cat input`será insignificante en comparación con echo $i, pero no en comparación con cat inputsolo, y en el caso de bash, la diferencia es en su mayor parte debido a bashhacer pequeñas lecturas. Cambiar de 128 a 4096 no tendrá influencia en el rendimiento de echo $i, pero ese no era el punto que estaba haciendo.
Stéphane Chazelas

También tenga en cuenta que el rendimiento de echo $ivariará considerablemente según el contenido de la entrada y el sistema de archivos (si contiene caracteres IFS o glob), por lo que no hice ninguna comparación de shells en eso en mi respuesta. Por ejemplo, aquí en la salida de yes | ghead -c50M, ksh93 es el más lento de todos, pero en adelante yes | ghead -c50M | paste -sd: -, es el más rápido.
Stéphane Chazelas

Cuando hablaba del tiempo total, hablaba de toda la implementación y sí, por supuesto, la división del campo ocurre con el comando echo. y aquí es donde se pasa la mayor parte del tiempo.
schily

Por supuesto, tiene razón en que el rendimiento depende del contenido de $ i.
schily

1

En ambos casos, el ciclo se ejecutará solo dos veces (una para la palabra seqy otra para la palabra 10).

Además, ambos fusionarán espacios en blanco adyacentes y eliminarán espacios en blanco iniciales / finales, de modo que la salida no sea necesariamente dos copias de la entrada.

primero

#!/bin/sh
for j in $(seq 10); do
    cat input
done >> output

Segundo

#!/bin/sh
i="$(cat input)"
for j in $(seq 10); do
    echo "$i"
done >> output

Una razón por la cual echoes más lenta puede ser que su variable sin comillas se está dividiendo en espacios en blanco en palabras separadas. Por 50 MB será mucho trabajo. ¡Cita las variables!

Le sugiero que corrija estos errores y luego vuelva a evaluar sus tiempos.


He probado esto localmente. Creé un archivo de 50 MB con la salida de tar cf - | dd bs=1M count=50. También extendí los bucles para que se ejecuten en un factor de x100 para que los tiempos se escalaran a un valor razonable (agregué un bucle adicional alrededor de todo su código: for k in $(seq 100); do... done). Aquí están los horarios:

time ./1.sh

real    0m5.948s
user    0m0.012s
sys     0m0.064s

time ./2.sh

real    0m5.639s
user    0m4.060s
sys     0m0.224s

Como puede ver, no hay una diferencia real, pero en todo caso, la versión que contiene echocorre un poco más rápido. Si elimino las comillas y ejecuto su versión dañada 2, el tiempo se duplica, lo que muestra que el shell tiene que hacer mucho más trabajo del que debería esperarse.

time ./2original.sh

real    0m12.498s
user    0m8.645s
sys     0m2.732s

En realidad, el ciclo se ejecuta 10 veces, no dos veces.
fpmurphy

Hice lo que dijiste, pero el problema no se ha resuelto. cates muy, muy rápido que echo. El primer script se ejecuta en un promedio de 3 segundos, pero el segundo se ejecuta en un promedio de 54 segundos.
Mohammad

@ fpmurphy1: No. Probé mi código. El ciclo se ejecuta solo dos veces, no 10 veces.
Mohammad

@ mohammad.k por tercera vez: si cita sus variables, el problema desaparece.
roaima

@roaima: ¿Qué hace el comando tar cf - | dd bs=1M count=50? ¿Hace un archivo normal con los mismos caracteres dentro? Si es así, en mi caso el archivo de entrada es completamente irregular con todo tipo de caracteres y espacios en blanco. Y de nuevo, usé timecomo lo ha usado, y el resultado fue el que dije: 54 segundos frente a 3 segundos.
Mohammad

-1

read es mucho más rápido que cat

Creo que todos pueden probar esto:

$ cd /sys/devices/system/cpu/cpu0/cpufreq
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do read p < scaling_cur_freq ; done

real    0m0.232s
user    0m0.139s
sys     0m0.088s
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do cat scaling_cur_freq > /dev/null ; done

real    0m9.372s
user    0m7.518s
sys     0m2.435s
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a read
read is a shell builtin
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a cat
cat is /bin/cat

cattoma 9.372 segundos. echotoma .232segundos

readEs 40 veces más rápido .

Mi primera prueba cuando $pse hizo eco en la pantalla revelada readfue 48 veces más rápida que cat.


-2

El echoestá destinado a poner 1 línea en la pantalla. Lo que haces en el segundo ejemplo es que pones el contenido del archivo en una variable y luego imprimes esa variable. En el primero, inmediatamente pone el contenido en la pantalla.

catestá optimizado para este uso. echono es. También poner 50Mb en una variable de entorno no es una buena idea.


Curioso. ¿Por qué no echoestaría optimizado para escribir texto?
roaima

2
No hay nada en el estándar POSIX que diga que echo debe poner una línea en una pantalla.
fpmurphy

-2

No se trata de que el eco sea más rápido, se trata de lo que estás haciendo:

En un caso, estás leyendo desde la entrada y escribiendo directamente en la salida. En otras palabras, todo lo que se lee desde la entrada a través de cat, va a la salida a través de stdout.

input -> output

En el otro caso, está leyendo desde la entrada a una variable en la memoria y luego escribiendo el contenido de la variable en la salida.

input -> variable
variable -> output

Este último será mucho más lento, especialmente si la entrada es de 50 MB.


Creo que hay que mencionar que cat tiene que abrir el archivo además de copiarlo desde stdin y escribirlo en stdout. Esta es la excelencia del segundo guión, pero el primero es muy mejor que el segundo en total.
Mohammad

No hay excelencia en el segundo guión; cat necesita abrir el archivo de entrada en ambos casos. En el primer caso, el stdout de cat va directamente al archivo. En el segundo caso, el stdout de cat va primero a una variable y luego imprime la variable en el archivo de salida.
Aleksander

@ mohammad.k, enfáticamente no hay "excelencia" en el segundo guión.
Comodín
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.