¿Cómo escribe 'yes' al archivo tan rápido?

58

Déjame dar un ejemplo:

$ timeout 1 yes "GNU" > file1
$ wc -l file1
11504640 file1

$ for ((sec0=`date +%S`;sec<=$(($sec0+5));sec=`date +%S`)); do echo "GNU" >> file2; done
$ wc -l file2
1953 file2

Aquí puede ver que el comando yesescribe 11504640líneas en un segundo, mientras que solo puedo escribir 1953líneas en 5 segundos usando bash fory echo.

Como se sugiere en los comentarios, hay varios trucos para hacerlo más eficiente, pero ninguno se acerca a la velocidad de yes:

$ ( while :; do echo "GNU" >> file3; done) & pid=$! ; sleep 1 ; kill $pid
[1] 3054
$ wc -l file3
19596 file3

$ timeout 1 bash -c 'while true; do echo "GNU" >> file4; done'
$ wc -l file4
18912 file4

Estos pueden escribir hasta 20 mil líneas en un segundo. Y se pueden mejorar aún más para:

$ timeout 1 bash -c 'while true; do echo "GNU"; done >> file5' 
$ wc -l file5
34517 file5

$ ( while :; do echo "GNU"; done >> file6 ) & pid=$! ; sleep 1 ; kill $pid
[1] 5690
$ wc -l file6
40961 file6

Estos nos llevan hasta 40 mil líneas en un segundo. Mejor, ¡pero aún está muy lejos de yespoder escribir alrededor de 11 millones de líneas en un segundo!

Entonces, ¿cómo se yesescribe en el archivo tan rápido?

Pandya
fuente
99
En el segundo ejemplo, tiene dos invocaciones de comandos externos para cada iteración del bucle, y datees algo pesado, además el shell tiene que volver a abrir la secuencia de salida echopara cada iteración del bucle. En el primer ejemplo, solo hay una invocación de comando único con una única redirección de salida, y el comando es extremadamente ligero. Los dos no son de ninguna manera comparables.
un CVn
@ MichaelKjörling tiene razón datepuede ser pesado, vea la edición de mi pregunta.
Pandya
1
timeout 1 $(while true; do echo "GNU">>file2; done;)es la forma incorrecta de usar timeout ya que el timeoutcomando solo comenzará una vez que finalice la sustitución del comando. Utilizar timeout 1 sh -c 'while true; do echo "GNU">>file2; done'.
Muru
1
resumen de las respuestas: al dedicar solo tiempo de CPU a las write(2)llamadas del sistema, no a las cargas de otros syscalls, la sobrecarga de shell o incluso la creación de procesos en su primer ejemplo (que se ejecuta y espera datecada línea impresa en el archivo). Un segundo de escritura es apenas suficiente para que se produzca un cuello de botella en la E / S de disco (en lugar de CPU / memoria), en un sistema moderno con mucha RAM. Si se le permite correr más tiempo, la diferencia sería menor. (Dependiendo de cuán mala sea la implementación de bash que use y la velocidad relativa de la CPU y el disco, es posible que ni siquiera sature la E / S del disco con bash).
Peter Cordes

Respuestas:

65

cáscara de nuez:

yesexhibe un comportamiento similar a la mayoría de las otras utilidades estándar que normalmente escriben en un FILE STREAM con salida almacenada en el búfer por el libC a través de stdio . Estos solo hacen la llamada al sistema write()cada 4kb (16kb o 64kb) o cualquiera que sea el bloque de salida BUFSIZ . echoes un write()per GNU. Eso es una gran cantidad de cambio de modo (que aparentemente no es tan costoso como un cambio de contexto ) .

Y eso no es en absoluto mencionar que, además de su bucle de optimización inicial, yeses un bucle C muy simple, pequeño y compilado y su bucle de shell no es de ninguna manera comparable a un programa optimizado de compilador.


pero estaba equivocado:

Cuando dije antes que yesusaba stdio, solo asumí que lo hizo porque se comporta de manera muy parecida a las que lo hacen. Esto no era correcto, solo emula su comportamiento de esta manera. Lo que realmente hace es muy similar a lo que hice a continuación con el shell: primero realiza un bucle para combinar sus argumentos (o ysi no hay ninguno) hasta que no crezcan más sin excederse BUFSIZ.

Un comentario de la fuente que precede inmediatamente a los forestados de bucle relevantes :

/* Buffer data locally once, rather than having the
large overhead of stdio buffering each item.  */

yeshace su hace lo suyo write()a partir de entonces.


digresión:

(Como se incluyó originalmente en la pregunta y se retuvo para el contexto de una explicación posiblemente informativa ya escrita aquí) :

He intentado timeout 1 $(while true; do echo "GNU">>file2; done;)pero no puedo detener el bucle.

El timeoutproblema que tiene con la sustitución de comandos: creo que lo entiendo ahora y puedo explicar por qué no se detiene. timeoutno se inicia porque su línea de comandos nunca se ejecuta. Su caparazón bifurca un caparazón secundario, abre una tubería en su stdout y lo lee. Dejará de leer cuando el niño deje de fumar, y luego interpretará todo lo que el niño escribió para la $IFSexpansión y la expansión global, y con los resultados reemplazará todo, desde $(la coincidencia ).

Pero si el niño es un bucle sin fin que nunca escribe en la tubería, entonces el niño nunca deja de hacerlo, y timeoutla línea de comandos nunca se completa antes (como supongo) que haces CTRL-Cy matas el bucle hijo. Por timeoutlo tanto , nunca puede matar el ciclo que debe completarse antes de que pueda comenzar.


otros timeouts:

... simplemente no son tan relevantes para sus problemas de rendimiento como la cantidad de tiempo que su programa de shell debe pasar cambiando entre los modos de usuario y kernel para manejar la salida. timeout, sin embargo, no es tan flexible como podría ser un shell para este propósito: donde los shells sobresalen en su capacidad de manipular argumentos y gestionar otros procesos.

Como se señala en otra parte, simplemente moviendo su [fd-num] >> named_fileredirección al destino de salida del bucle en lugar de solo dirigir la salida allí para el comando en bucle puede mejorar sustancialmente el rendimiento porque de esa manera al menos la open()llamada al sistema solo debe hacerse de una vez. Esto también se hace a continuación con la |tubería destinada como salida para los bucles internos.


comparación directa:

Puede que te guste:

for cmd in  exec\ yes 'while echo y; do :; done'
do      set +m
        sh  -c '{ sleep 1; kill "$$"; }&'"$cmd" | wc -l
        set -m
done

256659456
505401

Es algo así como la sub-relación de comando descrita anteriormente, pero no hay canalización y el elemento secundario está en segundo plano hasta que mata al elemento primario. En el yescaso de que el padre haya sido reemplazado desde que se generó el hijo, pero el shell llama yessuperponiendo su propio proceso con el nuevo, por lo que el PID sigue siendo el mismo y su hijo zombie todavía sabe a quién matar después de todo.


tampón más grande:

Ahora veamos cómo aumentar el write()búfer del shell .

IFS="
";    set y ""              ### sets up the macro expansion       
until [ "${512+1}" ]        ### gather at least 512 args
do    set "$@$@";done       ### exponentially expands "$@"
printf %s "$*"| wc -c       ### 1 write of 512 concatenated "y\n"'s  

1024

Elegí ese número porque las cadenas de salida de más de 1 kb se dividían en separaciones write()para mí. Y aquí está el bucle nuevamente:

for cmd in 'exec  yes' \
           'until [ "${512+:}" ]; do set "$@$@"; done
            while printf %s "$*"; do :; done'
do      set +m
        sh  -c $'IFS="\n"; { sleep 1; kill "$$"; }&'"$cmd" shyes y ""| wc -l
        set -m
done

268627968
15850496

Eso es 300 veces la cantidad de datos escritos por el shell en la misma cantidad de tiempo para esta prueba que la anterior. No está nada mal. Pero no lo es yes.


relacionado:

Según lo solicitado, hay una descripción más completa que los simples comentarios de código sobre lo que se hace aquí en este enlace .

mikeserv
fuente
@heemayl - tal vez? No estoy totalmente seguro de entender lo que estás preguntando. cuando un programa usa stdio para escribir la salida, lo hace sin búfer (como stderr por defecto) o búfer de línea (a terminales por defecto) o bloqueo de búfer (básicamente, la mayoría de las otras cosas se configuran de esta manera por defecto) . No estoy claro qué establece el tamaño del búfer de salida, pero generalmente es de 4kb. y entonces las funciones stdio lib reunirán su salida hasta que puedan escribir un bloque completo. ddes una herramienta estándar que definitivamente no usa stdio, por ejemplo. la mayoría de los demás lo hacen.
mikeserv
3
La versión de shell está haciendo open(existente) writeAND close(que creo que todavía espera el vaciado), Y está creando un nuevo proceso y ejecutándose date, para cada ciclo.
dave_thompson_085
@ dave_thompson_085 - vaya a / dev / chat . y lo que dices no es necesariamente cierto, como puedes ver allí. Por ejemplo, al hacer ese wc -lciclo bashconmigo, obtengo 1/5 de la salida que realiza el shciclo: bashadministra un poco más de 100k writes()a dashlos 500k.
mikeserv
Lo siento, era ambiguo; Me refería a la versión de shell en la pregunta, que en el momento en que lo leí solo tenía la versión original con el for((sec0=`date +%S`;...tiempo de control y la redirección en el bucle, no las mejoras posteriores.
dave_thompson_085
@ dave_thompson_085 - está bien. de todos modos, la respuesta fue incorrecta sobre algunos puntos fundamentales, y debería ser bastante correcta ahora, como espero.
mikeserv
20

Una mejor pregunta sería por qué su shell escribe el archivo tan lentamente. Cualquier programa compilado autónomo que use llamadas de sistema para escribir archivos de manera responsable (sin enjuagar todos los caracteres a la vez) lo haría razonablemente rápido. Lo que está haciendo es escribir líneas en un lenguaje interpretado (el shell), y además realiza muchas operaciones de entrada y salida innecesarias. Que yeshace:

  • abre un archivo para escribir
  • llama a funciones optimizadas y compiladas para escribir en una secuencia
  • el flujo se almacena en búfer, por lo que una llamada al sistema (un cambio costoso al modo kernel) ocurre muy raramente, en fragmentos grandes
  • cierra un archivo

Lo que hace tu guión:

  • lee en una línea de código
  • interpreta el código, realizando muchas operaciones adicionales para analizar su entrada y descubrir qué hacer
  • para cada iteración del ciclo while (que probablemente no sea barato en un lenguaje interpretado):
    • llame al datecomando externo y almacene su salida (solo en la versión original; en la versión revisada obtiene un factor de 10 al no hacerlo)
    • probar si se cumple la condición de terminación del bucle
    • abrir un archivo en modo agregar
    • analizar el echocomando, reconocerlo (con algún código de coincidencia de patrones) como un shell incorporado, llamar a la expansión de parámetros y todo lo demás en el argumento "GNU", y finalmente escribir la línea en el archivo abierto
    • cierra el archivo nuevamente
    • repite el proceso

Las partes costosas: toda la interpretación es extremadamente costosa (bash está haciendo un gran preprocesamiento de toda la entrada; su cadena podría contener una sustitución variable, sustitución de procesos, expansión de llaves, caracteres de escape y más), cada llamada de un builtin es probablemente una declaración de cambio con redireccionamiento a una función que se ocupe de la función incorporada, y lo más importante, abra y cierre un archivo para cada línea de salida. Podrías poner >> filefuera del ciclo while para hacerlo mucho más rápido , pero aún estás en un lenguaje interpretado. Eres bastante afortunado de queechoes un shell incorporado, no un comando externo; de lo contrario, su ciclo implicaría crear un nuevo proceso (fork y exec) en cada iteración. Lo que detendría el proceso: viste lo costoso que era cuando tenías el datecomando en el bucle.

Orión
fuente
11

Las otras respuestas han abordado los puntos principales. En una nota al margen, puede aumentar el rendimiento de su ciclo while escribiendo en el archivo de salida al final del cálculo. Comparar:

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU" >>/tmp/f; done;

real    0m0.080s
user    0m0.032s
sys     0m0.037s

con

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU"; done>>/tmp/f;

real    0m0.030s
user    0m0.019s
sys     0m0.011s
Apoorv Gupta
fuente
Sí, esto importa y la velocidad de escritura (al menos) se duplica en mi caso
Pandya