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 manualechose dice que muestra "una línea de texto" y supongo que está optimizado solo para variables cortas, no para variables muy largas comoi. 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.

cat $(for i in $(seq 1 10); do echo "input"; done) >> output? :)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.printf '%s' "$i", noecho $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/…Respuestas:
Hay varias cosas a considerar aquí.
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), yyashrealiza 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 deecho).Si desea optimizarlo aún más, en lugar de invocar
catvarias veces, simplemente paseinputvarias veces acat.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: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:Pero eso nunca será más eficiente que usarlo
caten el bucle (a menos que la entrada sea muy pequeña).fuente
/bin/echo $(perl -e 'print "A"x999999')dd bs=128 < input > /dev/nullcondd bs=64 < input > /dev/null. De los 0.6s que toma bash para leer ese archivo, 0.4 se gastan en esasreadllamadas al sistema en mis pruebas, mientras que otros shells pasan mucho menos tiempo allí.readwc()ytrim()en Burne Shell ocupan el 30% de todo el tiempo y esto probablemente se subestima, ya que no hay una biblioteca congprofanotacionesmbtowc().\xexpande?El problema no se trata
catyechose trata de la variable de cotización olvidada$i.En el script de shell similar a Bourne (excepto
zsh), dejar variables entre comillas causaglob+splitoperadores en las variables.es en realidad:
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 contenidoecho(Reemplazarloechopor externo/bin/echole dará la lista de argumentos demasiado tiempo o sin memoria) Depende del$itamaño). La mayor parte de laechoimplementació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 ycatrealizará la copia de E / S. El sistema también puede almacenar en caché el contenido del archivo para acelerar el proceso cat.fuente
/*/*/*/*../../../../*/*/*/*/../../../../puede estar en el contenido del archivo. Solo quiero señalar los detalles .time echo $( <xdditg106) >/dev/null real 0m0.125s user 0m0.085s sys 0m0.025stime echo "$( <xdditg106)" >/dev/null real 0m0.047s user 0m0.016s sys 0m0.022sglob+splitparte, y acelerará el ciclo while. Y también noté que no te ayudará mucho. Desde cuando la mayoría delechocomportamiento del shell no es compatible con POSIX.printf '%s' "$i"es mejor.Si llamas
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:
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.
fuente
i=`cat input`comando no divide los campos, es elecho $ique lo hace. El tiempo dedicadoi=`cat input`será insignificante en comparación conecho $i, pero no en comparación concat inputsolo, y en el caso debash, la diferencia es en su mayor parte debido abashhacer pequeñas lecturas. Cambiar de 128 a 4096 no tendrá influencia en el rendimiento deecho $i, pero ese no era el punto que estaba haciendo.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 deyes | ghead -c50M, ksh93 es el más lento de todos, pero en adelanteyes | ghead -c50M | paste -sd: -, es el más rápido.En ambos casos, el ciclo se ejecutará solo dos veces (una para la palabra
seqy otra para la palabra10).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
Segundo
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: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.fuente
cates muy, muy rápido queecho. El primer script se ejecuta en un promedio de 3 segundos, pero el segundo se ejecuta en un promedio de 54 segundos.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.reades mucho más rápido quecatCreo que todos pueden probar esto:
cattoma 9.372 segundos.echotoma.232segundosreadEs 40 veces más rápido .Mi primera prueba cuando
$pse hizo eco en la pantalla reveladareadfue 48 veces más rápida quecat.fuente
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.fuente
echoestaría optimizado para escribir texto?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.
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.
Este último será mucho más lento, especialmente si la entrada es de 50 MB.
fuente