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 cat
eso 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 i
fue 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 echo
y las cat
comparé:
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 manualecho
se 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 10
lugar 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: cat
funciona mucho más rápido que echo
.
cat $(for i in $(seq 1 10); do echo "input"; done) >> output
? :)echo
es 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
i
variable 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
bash
ser 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.zsh
necesita realizar un procesamiento posterior para escapar de los bytes NUL (otros shells se rompen en los bytes NUL), yyash
realiza 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,
echo
está 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$IFS
caracteres en la cadena. Si$IFS
contiene 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\x
secuencias 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
cat
no 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.cat
leerá 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
printf
lugar deecho
).Si desea optimizarlo aún más, en lugar de invocar
cat
varias veces, simplemente paseinput
varias 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
printf
no está integrado) con:Pero eso nunca será más eficiente que usarlo
cat
en el bucle (a menos que la entrada sea muy pequeña).fuente
/bin/echo $(perl -e 'print "A"x999999')
dd bs=128 < input > /dev/null
condd bs=64 < input > /dev/null
. De los 0.6s que toma bash para leer ese archivo, 0.4 se gastan en esasread
llamadas 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 congprof
anotacionesmbtowc()
.\x
expande?El problema no se trata
cat
yecho
se trata de la variable de cotización olvidada$i
.En el script de shell similar a Bourne (excepto
zsh
), dejar variables entre comillas causaglob+split
operadores 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+split
pero no le ayudará mucho, ya que cuando el shell todavía necesita construir el argumento de cadena grande y escanear su contenidoecho
(Reemplazarloecho
por externo/bin/echo
le dará la lista de argumentos demasiado tiempo o sin memoria) Depende del$i
tamaño). La mayor parte de laecho
implementación no es compatible con POSIX, expandirá las\x
secuencias de barra invertida en los argumentos que recibió.Con
cat
, el shell solo necesita generar un proceso en cada iteración de bucle ycat
realizará 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.025s
time echo "$( <xdditg106)" >/dev/null real 0m0.047s user 0m0.016s sys 0m0.022s
glob+split
parte, y acelerará el ciclo while. Y también noté que no te ayudará mucho. Desde cuando la mayoría delecho
comportamiento 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 $i
debe 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 $i
que lo hace. El tiempo dedicadoi=`cat input`
será insignificante en comparación conecho $i
, pero no en comparación concat input
solo, y en el caso debash
, la diferencia es en su mayor parte debido abash
hacer 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 $i
variará 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
seq
y 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
echo
es 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
echo
corre 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
cat
es 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étime
como lo ha usado, y el resultado fue el que dije: 54 segundos frente a 3 segundos.read
es mucho más rápido quecat
Creo que todos pueden probar esto:
cat
toma 9.372 segundos.echo
toma.232
segundosread
Es 40 veces más rápido .Mi primera prueba cuando
$p
se hizo eco en la pantalla reveladaread
fue 48 veces más rápida quecat
.fuente
El
echo
está 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.cat
está optimizado para este uso.echo
no es. También poner 50Mb en una variable de entorno no es una buena idea.fuente
echo
estarí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