2010-10-04 8 views
135

Siempre me ha sorprendido o frustrado el tiempo que lleva simplemente enviar a la terminal una declaración de impresión. Después de un registro lento y dolorosamente reciente, decidí investigarlo y me sorprendió bastante ver que casi todo el tiempo de espera está esperando que el terminal procese los resultados.¿Por qué la impresión en stdout es tan lenta? ¿Se puede acelerar?

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

Escribí una secuencia de comandos ('print_timer.py' en la parte inferior de esta pregunta) para comparar el tiempo al escribir 100k líneas a stdout, al 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 asegurarse de que el pitón no está haciendo algo detrás de las escenas como el reconocimiento de que reasignado salida estándar a/dev/null o algo así, hice el cambio de dirección fuera del guión ...

$ 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 una pitón truco, es solo la terminal. Siempre supe que la salida de dumping a/dev/null aceleraba las cosas, ¡pero nunca creí que fuera tan significativo!

Me sorprende lo lento que es el tty. ¿Cómo puede ser que escribir en el disco físico sea MUCHO más rápido que escribir en la "pantalla" (probablemente una opción de todas las RAM), y efectivamente es tan rápido como simplemente descargarlo a la basura con/dev/null?

This link habla de cómo bloquear el terminal de E/S para que pueda "analizar [la entrada], actualice su memoria intermedia de trama, comunicarse con el servidor X con el fin de desplazar la ventana y así sucesivamente" ... pero no lo entiendo del todo ¿Qué puede tomar tanto tiempo?

Espero que no haya escapatoria (¿menos que una implementación de tty más rápida?) Pero imagino que preguntaría de todos modos.


ACTUALIZACIÓN: después de leer algunos comentarios que he preguntado cuánto impacto el tamaño de mi pantalla tiene en realidad en el momento de la impresión, y que tiene algún significado. Los números realmente lentos arriba son con mi terminal Gnome volado hasta 1920x1200. Si lo reduzco muy pequeño, obtengo ...

----- 
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 definitivamente mejor (~ 4x), pero no cambia mi pregunta. Solo agrega a mi pregunta, ya que no entiendo por qué la representación de la pantalla del terminal debe ralentizar la escritura de una aplicación a stdout. ¿Por qué mi programa necesita esperar a que continúe la representación de la pantalla?

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

Todavía tengo la esperanza de que haya una configuración tty que se puede cambiar desde el lado de la aplicación para mejorar este comportamiento del programador. Si esto es estrictamente un problema de aplicación de terminal, ¿entonces esto quizás ni siquiera pertenece a StackOverflow?

¿Qué me estoy perdiendo?


Aquí es el programa de Python utilizada para generar la sincronización:

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 
+7

El propósito de escribir en stdout es para que un humano pueda leer la salida. Ningún ser humano en el mundo puede leer 10,000 líneas de texto en 12 segundos, entonces, ¿cuál es el punto de hacer stdout más rápido? –

+6

@Seun Osewa: Un ejemplo (que impulsó mi pregunta) es cuando se hacen cosas como [impresión de depuración de estado de cuenta] (http://stackoverflow.com/questions/189562). Desea ejecutar su programa y ver los resultados a medida que ocurren. Obviamente tienes razón en que la mayoría de las líneas pasarán volando que no puedes ver, pero cuando ocurre una excepción (o presionas la declaración getch/raw_input/sleep condicional que colocaste con cuidado) quieres mirar la salida impresa directamente en lugar de constantemente tener que abrir o actualizar una vista de archivo. – Russ

+2

La depuración de sentencias de impresión es una de las razones por las cuales los dispositivos tty (es decir, terminales) predeterminan el almacenamiento intermedio en lugar del almacenamiento en bloque: la salida de depuración no es muy útil si el programa se cuelga y las últimas líneas de salida de depuración están todavía en un búfer en lugar de enjuagado a la terminal. –

Respuesta

71

¡Gracias por todos los comentarios! He terminado respondiéndolo yo mismo con tu ayuda. Sin embargo, se siente sucio respondiendo tu propia pregunta.

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

Respuesta: impresión a la salida estándar es no inherentemente lento. Es la terminal con la que trabajas que es lenta. Y tiene mucho que ver con el almacenamiento en búfer de E/S en el lado de la aplicación (por ejemplo, el búfer de archivos de Python). Vea abajo.

Pregunta 2: ¿Se puede acelerar?

Respuesta: Sí, puede, pero aparentemente no desde el lado del programa (el lado que está 'imprimiendo' a stdout). Para acelerarlo, use un emulador de terminal diferente más rápido.

Explicación ...

me trató un programa de auto-describe 'ligero' terminal llamado wterm y se significativamente mejores resultados. A continuación se muestra la salida de mi escritura de la prueba (en el fondo de la cuestión) cuando se ejecuta en wterm a 1920x1200 en el mismo sistema en el que 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.26s es mucho mejor que 12s! No sé si wterm es más inteligente acerca de cómo representa la pantalla a lo largo de las líneas de cómo estaba sugiriendo (representa la cola "visible" a una velocidad de fotogramas razonable), o si simplemente "hace menos" que gnome-terminal. A los fines de mi pregunta, tengo la respuesta, sin embargo. gnome-terminal es lento.

Por lo tanto, si tiene un script de larga duración que cree que es lento y arroja grandes cantidades de texto a stdout ... pruebe con un terminal diferente y vea si es mejor.

Tenga en cuenta que casi al azar saqué wterm de los repositorios ubuntu/debian. This link podría ser la misma terminal, pero no estoy seguro. No probé ningún otro emulador de terminal.


Actualización: Porque tenía que rascarse la picazón, he probado todo un montón de otros emuladores de terminal con el mismo guión y de pantalla completa (1920x1200). Mis estadísticas recopiladas manualmente están 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 fueron bastante consistentes. Grabé el mejor valor (ish). YMMV, obviamente.

¡Como beneficio adicional, fue una visita interesante a algunos de los varios emuladores de terminal disponibles en el mercado! Estoy sorprendido de que mi primera prueba "alternativa" resulte ser la mejor del grupo.

+1

También puedes probar aterm. Aquí están los resultados en mi prueba usando su script. Aterm - imprimir: 0.491 s, escribir en el archivo (+ fsync): 0.110 s, imprimir con stdout =/dev/null: 0.087 s wterm - imprimir: 0.521 s, escribir al archivo (+ fsync): 0.105 s , imprimir con stdout =/dev/null: 0.085 s – frogstarr78

+0

@ frogstarr78: Gracias por el consejo ... Lo agregaré a la lista. Me pongo un poco peor, pero lo suficientemente cerca. Todos los números son para una sensación general, de todos modos. – Russ

+1

¿Cómo se compara urxvt con rxvt? – Daenyth

1

impresión al terminal va a ser lento. Desafortunadamente, a menos que escriba una nueva implementación de terminal, realmente no veo cómo aceleraría esto significativamente.

11

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

Es probable que stdout esté almacenado en la línea cuando apunta a un terminal (el mismo que el comportamiento de la secuencia stdout de C).

Como experimento divertido, intente conectar 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 
+0

No pensé en Python verificando su salida FS. Me pregunto si Python está haciendo un truco detrás de las escenas? No lo espero, pero no lo sé. – Russ

+0

+1 para señalar la diferencia más importante en el almacenamiento en memoria intermedia –

+0

@Russ: la opción '-u' fuerza' stdin', 'stdout' y' stderr' a ser sin búfer, que será más lento que el almacenamiento en bloque (debido a overhead) – Hasturkun

3

que no se puede hablar de los detalles técnicos porque yo no los conozco, pero esto no me sorprende: el terminal no fue diseñado para la impresión de una gran cantidad de datos como estos. De hecho, incluso proporciona un enlace a una carga de cosas de GUI que tiene que hacer cada vez que desea imprimir algo. Tenga en cuenta que si llama al script con pythonw, no tardará 15 segundos; esto es completamente un problema GUI. Redirigir 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): 
    ... 
1

Además de la salida, probablemente por defecto a un modo de línea de búfer, la salida a un terminal también está causando sus datos a fluir en un terminal y la línea serie, con un máximo rendimiento, o un pseudo-terminal y un proceso separado que maneja un bucle de evento de visualización, representa caracteres de alguna fuente, mueve bits de visualización para implementar una pantalla de desplazamiento. Este último escenario probablemente se distribuya en 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 latencia y cambio de contexto.

+0

¡Es cierto! Esto me llevó a intentar reducir el tamaño de mi ventana de terminal (en Gnome) a algo insignificante (desde 1920x1200). Efectivamente ... 2.8s tiempo de impresión vs 11.5s. Mucho mejor, pero aún así ... ¿por qué se está estancando? Se podría pensar que el búfer estándar (hmm) podría manejar todas las 100k líneas y la pantalla de la terminal simplemente tomaría lo que sea que pueda caber en la pantalla desde el final del búfer y lo haría en una toma rápida. – Russ

+0

El xterm (o gterm, en este caso) haría que su eventual pantalla sea más rápida si no creyera que también tenía que mostrar todas las otras salidas en el camino. Si intentara seguir esta ruta, probablemente el caso común de las actualizaciones de pantalla pequeña parezca menos receptivo. Al escribir este tipo de software, a veces puede tratarlo teniendo diferentes modos e intentando detectar cuándo necesita moverse de un modo de operación pequeño a grueso. Puede usar 'cat big_file | tail' o incluso 'cat big_file | tee big_file.cpy | cola' muy a menudo para esta aceleración. – nategoose

127

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

Enhorabuena, acaba de descubrir la importancia del almacenamiento en búfer de E/S. :-)

El disco aparece para ser más rápido, ya que es muy amortiguada: write() todas las llamadas de Python están regresando antes de que algo está realmente escrito en el disco físico. (El sistema operativo lo hace más tarde, la combinación de muchos miles de escrituras individuales en una trozos grandes y eficientes.)

El terminal, por otro lado, hace poco o sin almacenamiento temporal: cada individuo print/write(line) espera a que el completa escribir (es decir, mostrar en el dispositivo de salida) para completar.

Para que la comparación equitativa, debe realizar la prueba de archivos, utilice el mismo búfer de salida como el terminal, lo que se puede hacer mediante la modificación de su ejemplo para:

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 

me encontré con el examen de escritura de archivos en mi máquina, y con buffering, también 0.05s aquí para 100,000 líneas.

Sin embargo, con las modificaciones anteriores para escribir sin búfer, lleva solo 40 segundos escribir solo 1.000 líneas en el disco. Me rendí esperando a que escribieran 100.000 líneas, pero extrapolando desde el anterior, tomaría durante una hora.

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

Para responder a su pregunta original, escribir en un terminal es realmente increíblemente rápido, teniendo en cuenta todo, y no hay mucho espacio para hacerlo mucho más rápido (pero los terminales individuales varían en la cantidad de trabajo que realizan; Comentario de Russ a esta respuesta).

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

+4

Obtengo almacenamiento en búfer de E/S ... sin duda me recordó que debería haberme sincronizado para una verdadera comparación del tiempo de finalización (actualizaré la pregunta), pero una fsync _per línea_ es una locura. ¿Realmente un tty necesita hacer eso efectivamente? ¿No hay un buffer de terminal/OS equivalente para los archivos?es decir: las aplicaciones escriben en stdout y regresan antes de que rinda el terminal a la pantalla, con el terminal (u os) almacenando todo. El terminal podría renderizar sensiblemente la cola a la pantalla a una velocidad de cuadro visible. El bloqueo efectivo en cada línea parece tonto. Siento que todavía me estoy perdiendo algo. – Russ

+0

Puede abrir un identificador para stdout con un buffer grande, usando algo como 'os.fdopen (sys.stdout.fileno(), 'w', BIGNUM)'. Sin embargo, esto casi nunca sería útil: casi todas las aplicaciones tendrían que recordar descargar explícitamente después de cada línea de salida prevista por el usuario. –

+1

Experimenté anteriormente con grandes (hasta 10MB con 'fp = os.fdopen (sys .__ stdout __. Fileno(), 'w', 10000000)') buffers de python. El impacto fue nulo. es decir, aún demoras largas en tty. Esto me hizo pensar/darse cuenta de que simplemente pospones el lento problema de tty ... cuando el búfer de Python finalmente descarga el tty parece hacer la misma cantidad total de procesamiento en la transmisión antes de regresar. – Russ

Cuestiones relacionadas