¿Por qué la impresión a salida estándar es tan lenta? ¿Se puede acelerar?


Siempre me ha sorprendido/frustrado con el tiempo que se tarda en simplemente la salida a la terminal con una instrucción de impresión. Después de un registro reciente dolorosamente lento, decidí investigarlo y me sorprendió bastante encontrar que casi todo el tiempo dedicado está esperando que el terminal procese los resultados.

¿Se puede acelerar la escritura a stdout de alguna manera?

Escribí un script ('print_timer.py ' al final de esta pregunta) para comparar el tiempo al escribir líneas de 100k a stdout, a archivo, y con stdout redirigido a /dev/null. Aquí está el resultado del tiempo:

$ python print_timer.py
this is a test
this is a test
<snipped 99997 lines>
this is a test
-----
timing summary (100k lines each)
-----
print                         :11.950 s
write to file (+ fsync)       : 0.122 s
print with stdout = /dev/null : 0.050 s

Wow. Para asegurarme de que python no está haciendo algo detrás de escena como reconocer que reasigné stdout a /dev/null o algo así, hice la redirección fuera del script...

$ python print_timer.py > /dev/null
-----
timing summary (100k lines each)
-----
print                         : 0.053 s
write to file (+fsync)        : 0.108 s
print with stdout = /dev/null : 0.045 s

Así que no es un truco de python, es solo el terminal. Siempre supe que el dumping de salida a /dev/null aceleró las cosas, pero nunca pensé que era que importante!

Me sorprende lo lento que es el tty. ¿Cómo puede sea que escribir en el disco físico es mucho más rápido que escribir en la "pantalla" (presumiblemente un op all-RAM), y es efectivamente tan rápido como simplemente tirar a la basura con /dev/null?

Este enlace habla de cómo el terminal bloqueará E/S para que pueda "analizar [la entrada], actualizar su búfer de fotogramas, comunicarse con el servidor X para desplazarse por la ventana y así sucesivamente"... pero no lo entiendo del todo. ¿Qué puede tardar tanto?

Espero que no haya salida (¿falta una implementación de tty más rápida? pero me imagino que preguntaría de todos modos.


ACTUALIZACIÓN: después de leer algunos comentarios, me pregunté cuánto impacto tiene el tamaño de mi pantalla en el tiempo de impresión, y tiene algún significado. Los números realmente lentos de arriba son con mi terminal Gnome hinchado a 1920x1200. Si lo reduzco muy pequeño me pongo...

-----
timing summary (100k lines each)
-----
print                         : 2.920 s
write to file (+fsync)        : 0.121 s
print with stdout = /dev/null : 0.048 s

Eso es ciertamente mejor (~4x), pero no cambia mi pregunta. Solo añade a mi pregunta ya que no entiendo por qué la representación de la pantalla del terminal debería ralentizar la escritura de una aplicación en stdout. ¿Por qué mi programa tiene que esperar a que continúe el renderizado de la pantalla?

¿No se crean todas las aplicaciones de terminal/tty iguales? Todavía tengo que experimentar. Realmente me parece que un terminal debería ser capaz de almacenar en búfer todos los datos entrantes, analizarlos/renderizarlos de forma invisible, y solo renderizar el fragmento más reciente que es visible en la configuración de pantalla actual a una velocidad de fotogramas razonable. Así que si puedo escribir+fsync al disco en ~0.1 segundos, un terminal debería ser capaz de completar la misma operación en algo de ese orden (con tal vez algunas actualizaciones de pantalla mientras lo hacía).

Todavía espero que haya una configuración tty que se pueda cambiar desde el lado de la aplicación para mejorar este comportamiento para el programador. Si esto es estrictamente un problema de aplicación de terminal, entonces esto tal vez ni siquiera pertenece a StackOverflow?

¿Qué me estoy perdiendo?


Aquí está el programa python utilizado para generar tiempo:

import time, sys, tty
import os

lineCount = 100000
line = "this is a test"
summary = ""

cmd = "print"
startTime_s = time.time()
for x in range(lineCount):
    print line
t = time.time() - startTime_s
summary += "%-30s:%6.3f s\n" % (cmd, t)

#Add a newline to match line outputs above...
line += "\n"

cmd = "write to file (+fsync)"
fp = file("out.txt", "w")
startTime_s = time.time()
for x in range(lineCount):
    fp.write(line)
os.fsync(fp.fileno())
t = time.time() - startTime_s
summary += "%-30s:%6.3f s\n" % (cmd, t)

cmd = "print with stdout = /dev/null"
sys.stdout = file(os.devnull, "w")
startTime_s = time.time()
for x in range(lineCount):
    fp.write(line)
t = time.time() - startTime_s
summary += "%-30s:%6.3f s\n" % (cmd, t)

print >> sys.stderr, "-----"
print >> sys.stderr, "timing summary (100k lines each)"
print >> sys.stderr, "-----"
print >> sys.stderr, summary
Author: slm, 2010-10-04

6 answers

¿Cómo puede ser que escribir en un disco físico sea mucho más rápido que escribir en la "pantalla" (presumiblemente un op all-RAM), y sea efectivamente tan rápido como simplemente volcarse a la basura con /dev/null?

Felicitaciones, acaba de descubrir la importancia del buffering de E/S. :-)

El disco parece ser más rápido, porque está altamente almacenado en búfer: todas las llamadas de Python write() regresan antes de que nada se escriba realmente en el disco físico. (El sistema operativo hace esto más tarde, combinando muchos miles de escrituras individuales en grandes y eficientes trozos.)

El terminal, por otro lado, hace poco o nada de buffering: cada individuo print / write(line) espera a que la escritura completa (es decir, mostrar al dispositivo de salida) se complete.

Para hacer la comparación justa, debe hacer que la prueba de archivo use el mismo búfer de salida que el terminal, lo que puede hacer modificando su ejemplo a:

fp = file("out.txt", "w", 1)   # line-buffered, like stdout
[...]
for x in range(lineCount):
    fp.write(line)
    os.fsync(fp.fileno())      # wait for the write to actually complete

Ejecuté su prueba de escritura de archivos en mi máquina, y con buffering, también 0.05 s aquí para 100,000 líneas.

Sin embargo, con las modificaciones anteriores para escribir sin búfer, toma 40 segundos escribir solo 1.000 líneas en el disco. Dejé de esperar 100.000 líneas para escribir, pero extrapolando de la anterior, tomaría más de una hora.

Eso pone los 11 segundos de la terminal en perspectiva, ¿no es así?

Así que para responder a su pregunta original, escribir a una terminal es realmente increíblemente rápido, todas las cosas considerado, y no hay mucho espacio para hacerlo mucho más rápido (pero los terminales individuales varían en la cantidad de trabajo que hacen; ver el comentario de Russ a esta respuesta).

(Podría agregar más búfer de escritura, como con E/S de disco, pero entonces no vería lo que se escribió en su terminal hasta después de que el búfer se vacíe. Es una compensación: interactividad versus eficiencia masiva.)

 138
Author: Pi Delport,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-06 03:40:44

Gracias por todos los comentarios! Terminé contestándolo yo mismo con tu ayuda. Sin embargo, se siente sucio responder a tu propia pregunta.

Pregunta 1: ¿Por qué la impresión a stdout es lenta?

Respuesta: Imprimir en stdout es no inherentemente lento. Es la terminal con la que trabajas la que es lenta. Y tiene casi cero que ver con el almacenamiento en búfer de E/S en el lado de la aplicación (por ejemplo: almacenamiento en búfer de archivos python). Véase más adelante.

Pregunta 2: ¿Puede ser acelerado?

Respuesta: Sí puede, pero aparentemente no desde el lado del programa (el lado que hace la 'impresión' a stdout). Para acelerarlo, utilice un emulador de terminal diferente más rápido.

Explicación...

Probé un programa terminal auto-descrito 'ligero' llamado wterm y obtuve significativamente mejores resultados. A continuación se muestra la salida de mi script de prueba (en la parte inferior de la pregunta) cuando se ejecuta en wterm a 1920x1200 en el mismo sistema donde el la opción de impresión básica tomó 12s usando gnome-terminal:

-----
timing summary (100k lines each)
-----
print                         : 0.261 s
write to file (+fsync)        : 0.110 s
print with stdout = /dev/null : 0.050 s

0.26 s es mucho mejor que 12s! No se si wterm es más inteligente acerca de cómo se renderiza a la pantalla a lo largo de las líneas de cómo estaba sugiriendo (renderizar la cola 'visible' a una velocidad de fotogramas razonable), o si simplemente "hace menos" que gnome-terminal. Para los propósitos de mi pregunta tengo la respuesta, sin embargo. gnome-terminal es lento.

Así que - Si usted tiene un script de ejecución larga que usted siente que es lento y arroja cantidades masivas de texto a stdout... pruebe un terminal diferente y ver si es mejor!

Tenga en cuenta que saqué al azar wterm de los repositorios de ubuntu/debian. Este enlace puede ser el mismo terminal, pero no estoy seguro. No probé ningún otro emulador de terminal.


Actualización: Debido a que tuve que rascarme la picazón, probé un montón de otros emuladores de terminal con el mismo script y pantalla completa (1920x1200). Mis estadísticas recopiladas manualmente son aquí:

wterm           0.3s
aterm           0.3s
rxvt            0.3s
mrxvt           0.4s
konsole         0.6s
yakuake         0.7s
lxterminal        7s
xterm             9s
gnome-terminal   12s
xfce4-terminal   12s
vala-terminal    18s
xvt              48s

Los tiempos registrados se recopilan manualmente, pero eran bastante consistentes. Registré el mejor (ish) valor. YMMV, obviamente.

Como bono, fue un interesante recorrido por algunos de los varios emuladores de terminal disponibles por ahí! Me sorprende que mi primera prueba' alternativa ' resultó ser la mejor del grupo.

 78
Author: Russ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-10-07 20:05:33

Su redirección probablemente no hace nada, ya que los programas pueden determinar si su salida FD apunta a un tty.

Es probable que stdout sea una línea tamponada cuando apunta a una terminal (lo mismo que C stdout comportamiento de la corriente).

Como un experimento divertido, intente canalizar la salida a cat.


He intentado mi propio experimento divertido, y aquí están los resultados.

$ python test.py 2>foo
...
$ cat foo
-----
timing summary (100k lines each)
-----
print                         : 6.040 s
write to file                 : 0.122 s
print with stdout = /dev/null : 0.121 s

$ python test.py 2>foo |cat
...
$ cat foo
-----
timing summary (100k lines each)
-----
print                         : 1.024 s
write to file                 : 0.131 s
print with stdout = /dev/null : 0.122 s
 13
Author: Hasturkun,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-10-04 16:31:49

No puedo hablar de los detalles técnicos porque no los conozco, pero esto no me sorprende: el terminal no fue diseñado para imprimir muchos datos como este. De hecho, incluso proporciona un enlace a un montón de cosas GUI que tiene que hacer cada vez que desea imprimir algo! Tenga en cuenta que si llama al script con pythonw en su lugar, no tarda 15 segundos; esto es completamente un problema de interfaz gráfica de usuario. Redirige stdout a un archivo para evitar esto:

import contextlib, io
@contextlib.contextmanager
def redirect_stdout(stream):
    import sys
    sys.stdout = stream
    yield
    sys.stdout = sys.__stdout__

output = io.StringIO
with redirect_stdout(output):
    ...
 4
Author: Katriel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-10-04 16:25:39

La impresión en el terminal va a ser lenta. Desafortunadamente, a falta de escribir una nueva implementación de terminal, realmente no puedo ver cómo aceleraría esto significativamente.

 2
Author: shuttle87,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-10-04 16:20:19

Además de la salida probablemente por defecto a un modo de búfer de línea, la salida a un terminal también está causando que sus datos fluyan hacia un terminal y una línea serie con un rendimiento máximo, o un pseudo-terminal y un proceso separado que está manejando un bucle de eventos de visualización, renderizando caracteres de alguna fuente, moviendo bits de visualización para implementar una pantalla de desplazamiento. Este último escenario probablemente se extiende a través de múltiples procesos (por ejemplo, servidor/cliente telnet, aplicación de terminal, servidor de visualización X11), por lo que también hay problemas de cambio de contexto y latencia.

 2
Author: Liudvikas Bukys,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-10-04 16:52:39