Perché la stampa su stdout è così lenta? Può essere accelerato?


166

Sono sempre stato sorpreso / frustrato da quanto tempo impieghi semplicemente per inviare al terminale una dichiarazione stampata. Dopo alcune recenti registrazioni dolorosamente lente ho deciso di esaminarlo e sono rimasto piuttosto sorpreso di scoprire che quasi tutto il tempo trascorso è in attesa che il terminale elabori i risultati.

La scrittura su stdout può essere accelerata in qualche modo?

Ho scritto uno script (' print_timer.py' in fondo a questa domanda) per confrontare i tempi quando si scrivono 100k righe su stdout, su file e con reindirizzamento su stdout /dev/null. Ecco il risultato della tempistica:

$ 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. Per essere sicuro che Python non stia facendo qualcosa dietro le quinte come riconoscere che ho riassegnato stdout a / dev / null o qualcosa del genere, ho fatto il reindirizzamento fuori dallo 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

Quindi non è un trucco pitone, è solo il terminale. Ho sempre saputo scaricare l'output su / dev / null velocizzando le cose, ma non ho mai pensato che fosse così significativo!

Mi stupisce di quanto sia lento il tty. Come può essere che la scrittura su disco fisico sia VELOCEMENTE più veloce della scrittura sullo "schermo" (presumibilmente una operazione tutta RAM), ed è efficace quanto il semplice scaricamento nella spazzatura con / dev / null?

Questo link parla di come il terminale bloccherà l'I / O in modo che possa "analizzare [l'input], aggiornare il suo frame buffer, comunicare con il server X per scorrere la finestra e così via" ... ma io no ottenerlo completamente. Cosa può richiedere così tanto tempo?

Mi aspetto che non ci sia via d'uscita (a meno di un'implementazione tty più veloce?) Ma immagino che lo chiederei comunque.


AGGIORNAMENTO: dopo aver letto alcuni commenti, mi chiedevo quanto impatto la mia dimensione dello schermo ha effettivamente sul tempo di stampa, e ha un certo significato. I numeri molto lenti sopra sono con il mio terminale Gnome fatto saltare in aria a 1920x1200. Se lo riduco molto piccolo ottengo ...

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

È sicuramente meglio (~ 4x), ma non cambia la mia domanda. Aggiunge solo alla mia domanda poiché non capisco perché il rendering dello schermo del terminale dovrebbe rallentare un'applicazione che scrive su stdout. Perché il mio programma deve attendere che il rendering dello schermo continui?

Tutte le app terminal / tty non sono state create uguali? Devo ancora sperimentare. Mi sembra davvero che un terminale dovrebbe essere in grado di bufferizzare tutti i dati in arrivo, analizzarli / renderli in modo invisibile e renderizzare solo il blocco più recente che è visibile nella configurazione dello schermo corrente a un frame rate ragionevole. Quindi, se riesco a scrivere + fsync sul disco in ~ 0,1 secondi, un terminale dovrebbe essere in grado di completare la stessa operazione in qualcosa di quell'ordine (con forse alcuni aggiornamenti dello schermo mentre lo faceva).

Spero ancora che ci sia un'impostazione tty che può essere modificata dal lato dell'applicazione per rendere questo comportamento migliore per il programmatore. Se questo è strettamente un problema con l'applicazione terminale, forse questo non appartiene nemmeno a StackOverflow?

Cosa mi sto perdendo?


Ecco il programma Python utilizzato per generare i tempi:

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

9
L'intero scopo di scrivere su stdout è che un essere umano possa leggere l'output. Nessun essere umano al mondo è in grado di leggere 10.000 righe di testo in 12 secondi, quindi qual è il punto di rendere lo stdout più veloce ???
Seun Osewa,

14
@Seun Osewa: Un esempio (che ha guidato la mia domanda) è quando si eseguono operazioni come il debug delle istruzioni di stampa . Vuoi eseguire il tuo programma e vedere i risultati mentre si verificano. Hai ovviamente ragione sul fatto che la maggior parte delle linee passerà per quella che non riesci a vedere, ma quando si verifica un'eccezione (o colpisci l'istruzione condizionale getch / raw_input / sleep che hai posizionato con cura) vuoi guardare l'output di stampa direttamente anziché dover costantemente aprire o aggiornare una vista file.
Russ,

3
Il debug delle istruzioni di stampa è uno dei motivi per cui tty devices (ovvero i terminali) utilizza il buffering di riga anziché il buffering dei blocchi: l'output di debug non è molto utile se il programma si blocca e le ultime righe dell'output di debug sono ancora in un buffer anziché a filo con il terminale.
Stephen C. Steel,

@Stephen: Questo è il motivo per cui non mi sono preoccupato molto di perseguire gli enormi miglioramenti dichiarati da un commentatore aumentando le dimensioni del buffer. Elimina completamente lo scopo della stampa di debug! Ho sperimentato un po 'durante le indagini, ma non ho riscontrato alcun miglioramento netto. Sono ancora curioso della discrepanza, ma non proprio.
Russ,

A volte per programmi in esecuzione molto lunghi, stamperò solo lo stdout della riga corrente ogni n secondi, simile a un ritardo di aggiornamento in un'app maledizioni. Non è perfetto, ma dà un'idea di dove mi trovo di tanto in tanto.
rkulla,

Risposte:


155

Come può essere che la scrittura su disco fisico sia VELOCEMENTE più veloce della scrittura sullo "schermo" (presumibilmente una operazione tutta RAM), ed è efficace quanto il semplice scaricamento nella spazzatura con / dev / null?

Congratulazioni, hai appena scoperto l'importanza del buffering I / O. :-)

Il disco sembra essere più veloce, perché è altamente bufferizzato: tutte le write()chiamate di Python stanno tornando prima che qualcosa sia effettivamente scritto sul disco fisico. (Il sistema operativo lo fa in seguito, combinando molte migliaia di singole scritture in blocchi grandi ed efficienti.)

Il terminale, d'altra parte, esegue un buffering minimo o nullo: ogni individuo print/ write(line)attende il completamento della scrittura completa (ovvero la visualizzazione sul dispositivo di output).

Per rendere equo il confronto, è necessario fare in modo che il test del file utilizzi lo stesso buffer di output del terminale, cosa che è possibile fare modificando il proprio esempio in:

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

Ho eseguito il test di scrittura dei file sul mio computer e, con il buffering, è stato utilizzato anche 0,05 per 100.000 righe.

Tuttavia, con le modifiche di cui sopra per scrivere senza buffer, sono necessari 40 secondi per scrivere solo 1.000 righe sul disco. Ho rinunciato ad aspettare 100.000 righe per scrivere, ma estrapolando dalla precedente, ci sarebbe voluta più di un'ora .

Ciò mette in prospettiva gli 11 secondi del terminale, no?

Quindi, per rispondere alla tua domanda originale, scrivere su un terminale è in realtà incredibilmente veloce, tutto sommato, e non c'è molto spazio per renderlo molto più veloce (ma i singoli terminali variano in quanto lavoro fanno; vedi il commento di Russ a questo risposta).

(Potresti aggiungere più buffer di scrittura, come con l'I / O del disco, ma poi non vedresti ciò che è stato scritto sul tuo terminale fino a quando il buffer non viene scaricato. È un compromesso: interattività contro efficienza di massa.)


6
Ottengo il buffering I / O ... mi hai sicuramente ricordato che avrei dovuto fsync per un vero confronto tra i tempi di completamento (aggiornerò la domanda), ma un fsync per riga è follia. Un tty ha davvero bisogno di farlo efficacemente? Non esiste un buffer sul lato terminale / os equivalente a per i file? vale a dire: le applicazioni scrivono su stdout e ritornano prima che il terminale renda sullo schermo, con il terminale (o os) che esegue il buffering di tutto. Il terminale potrebbe quindi sensibilmente eseguire il rendering della coda sullo schermo con una frequenza dei fotogrammi visibile. Il blocco efficace su ogni linea sembra sciocco. Sento che mi manca ancora qualcosa.
Russ,

Puoi semplicemente aprire una maniglia per stdout con un grosso buffer tu stesso, usando qualcosa di simile os.fdopen(sys.stdout.fileno(), 'w', BIGNUM). Questo non sarebbe quasi mai utile, però: quasi tutte le applicazioni dovrebbero ricordare di svuotare esplicitamente dopo ogni riga di output previsto dall'utente.
Pi Delport,

1
In precedenza ho sperimentato enormi fp = os.fdopen(sys.__stdout__.fileno(), 'w', 10000000)buffer (fino a 10 MB con ) lato pitone. L'impatto è stato nullo. vale a dire: ritardi tty ancora lunghi. Questo mi ha fatto pensare / rendermi conto che hai appena posticipato il lento problema di tty ... quando il buffer di Python scarica finalmente il tty sembra ancora fare la stessa quantità totale di elaborazione sullo stream prima di tornare.
Russ,

8
Nota che questa risposta è fuorviante e sbagliata (scusate!). Nello specifico è sbagliato dire "non c'è molto spazio per renderlo più veloce [di 11 secondi]". Si prega di vedere la mia risposta alla domanda in cui mostro che il terminale Wterm ha ottenuto lo stesso risultato di 11 secondi in 0,26 secondi.
Russ,

2
Russ: grazie per il feedback! Da parte mia, un fdopenbuffer più grande (2 MB) ha sicuramente fatto un'enorme differenza: il tempo di stampa è passato da molti secondi a 0,05 secondi, lo stesso dell'output del file (usando gnome-terminal).
Pi Delport,

88

Grazie per tutti i commenti! Ho finito per rispondere da solo con il tuo aiuto. È sporco rispondere alla tua domanda, però.

Domanda 1: Perché la stampa su stdout è lenta?

Risposta: La stampa su stdout non è intrinsecamente lenta. È il terminale con cui lavori che è lento. E ha praticamente zero a che fare con il buffering I / O sul lato dell'applicazione (ad esempio: buffering dei file python). Vedi sotto.

Domanda 2: può essere accelerato?

Risposta: Sì, ma apparentemente non dal lato del programma (il lato che esegue la "stampa" su stdout). Per accelerarlo, utilizzare un emulatore di terminale diverso più veloce.

Spiegazione...

Ho provato a chiamare un programma terminale "leggero" che ho descritto wterme ho ottenuto risultati significativamente migliori. Di seguito è riportato l'output del mio script di test (in fondo alla domanda) quando si esegue wterma 1920x1200 sullo stesso sistema in cui l'opzione di stampa di base ha richiesto 12 secondi usando gnome-terminal:

-----
riepilogo dei tempi (100k righe ciascuno)
-----
stampa: 0,261 s
scrivi su file (+ fsync): 0.110 s
stampa con stdout = / dev / null: 0,050 s

0.26s è MOLTO meglio di 12s! Non so se wtermsia più intelligente su come viene visualizzato lo schermo secondo le linee che stavo suggerendo (rendere la coda "visibile" a un frame rate ragionevole), o se "fa meno" di gnome-terminal. Ai fini della mia domanda, però, ho la risposta. gnome-terminalè lento.

Quindi - Se hai uno script di lunga durata che ritieni lento e sputa enormi quantità di testo su stdout ... prova un terminale diverso e vedi se è meglio!

Si noti che ho praticamente estratto casualmente wtermdai repository ubuntu / debian. Questo link potrebbe essere lo stesso terminale, ma non sono sicuro. Non ho testato nessun altro emulatore di terminale.


Aggiornamento: poiché ho dovuto grattare il prurito, ho testato un'intera pila di altri emulatori terminali con lo stesso script e lo schermo intero (1920x1200). Le mie statistiche raccolte manualmente sono qui:

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
Terminale xfce4 12s
vala-terminal 18s
xvt 48s

I tempi registrati vengono raccolti manualmente, ma erano piuttosto coerenti. Ho registrato il miglior valore (ish). YMMV, ovviamente.

Come bonus, è stato un tour interessante di alcuni dei vari emulatori di terminali disponibili là fuori! Sono sorpreso che il mio primo test "alternativo" si sia rivelato il migliore del gruppo.


1
Potresti anche provare aterm. Ecco i risultati del mio test usando il tuo script. Aterm - stampa: 0,491 s, scrittura su file (+ fsync): 0,110 s, stampa con stdout = / dev / null: 0,087 s wterm - stampa: 0,521 s, scrittura su file (+ fsync): 0,105 s, stampa con stdout = / dev / null:
0.085

1
Come si confronta urxvt con rxvt?
Daenyth,

3
Inoltre, screen(il programma) dovrebbe essere incluso nell'elenco! (O byobu, che è un wrapper per i screenmiglioramenti) Questa utility consente di avere diversi terminali, proprio come le schede nei terminali X. Presumo che la stampa sul screenterminale corrente sia la stessa della stampa su uno semplice, ma che ne dici di stampare su uno dei screenterminali e poi passare a un altro senza attività?
Armando Pérez Marqués,

1
Strano, qualche tempo fa stavo confrontando diversi terminali in termini di velocità e gnome-terminal è risultato il migliore in test piuttosto seri mentre xterm era più lento. Forse hanno lavorato sodo sul buffering da allora. Anche il supporto Unicode potrebbe fare una grande differenza.
Tomas Pruzina,

2
iTerm2 su OSX mi ha dato: print: 0.587 s, write to file (+fsync): 0.034 s, print with stdout = /dev/null : 0.041 s. E con 'schermo' in esecuzione su iTerm2:print: 1.286 s, write to file (+fsync): 0.043 s, print with stdout = /dev/null : 0.033 s
rkulla,

13

Il reindirizzamento probabilmente non fa nulla poiché i programmi possono determinare se il loro FD di output punta a un tty.

È probabile che stdout sia bufferizzato in linea quando punta a un terminale (lo stesso del stdoutcomportamento del flusso di C ).

Come esperimento divertente, prova a reindirizzare l'output a cat.


Ho provato il mio divertente esperimento, ed ecco i risultati.

$ 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

Non pensavo a Python che controllava il suo output FS. Mi chiedo se Python stia tirando un trucco dietro le quinte? Non mi aspetto, ma non lo so.
Russ,

+1 per sottolineare la fondamentale differenza nel buffering
Peter G.

@Russ: l' -uopzione impone stdin, stdoute stderrdi essere senza buffer, che sarà più lento rispetto al buffer (a causa di sovraccarico)
Hasturkun

4

Non posso parlare dei dettagli tecnici perché non li conosco, ma questo non mi sorprende: il terminale non è stato progettato per stampare molti dati come questo. In effetti, fornisci anche un collegamento a un carico di elementi della GUI che deve fare ogni volta che vuoi stampare qualcosa! Si noti che se si chiama lo script con pythonwinvece, non ci vogliono 15 secondi; questo è interamente un problema con la GUI. Reindirizzare stdouta un file per evitare questo:

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):
    ...

3

La stampa sul terminale sarà lenta. Sfortunatamente, a meno di scrivere una nuova implementazione del terminale, non riesco davvero a vedere come lo accelereresti in modo significativo.


2

Oltre all'output probabilmente impostato su una modalità buffer di linea, l'output su un terminale sta causando anche il flusso dei dati in un terminale e una linea seriale con un throughput massimo, o uno pseudo-terminale e un processo separato che gestisce un display loop degli eventi, rendering dei caratteri da alcuni font, spostamento dei bit di visualizzazione per implementare una visualizzazione a scorrimento. Quest'ultimo scenario è probabilmente distribuito su più processi (ad es. Server / client telnet, app terminale, server di visualizzazione X11) quindi ci sono anche problemi di cambio di contesto e latenza.


Vero! Questo mi ha spinto a provare a ridurre le dimensioni della mia finestra del terminale (in Gnome) a qualcosa di scadente (da 1920x1200). Abbastanza sicuro ... tempo di stampa 2,8 s vs 11,5 s. Molto meglio, ma comunque ... perché si blocca? Si potrebbe pensare che il buffer stdout (hmm) sia in grado di gestire tutte le linee da 100k e il display del terminale catturerebbe semplicemente tutto ciò che può adattarsi sullo schermo dall'estremità della coda del buffer e lo farebbe in un colpo veloce.
Russ,

Il xterm (o gterm, in questo caso) renderebbe il tuo schermo più veloce se non pensasse di dover mostrare anche tutti gli altri output lungo la strada. Se tentasse di seguire questa strada, probabilmente renderebbe meno reattivo il caso comune degli aggiornamenti su schermi piccoli. Quando si scrive questo tipo di software, a volte è possibile gestirlo con modalità diverse e cercando di rilevare quando è necessario passare a / da una modalità operativa piccola a più ampia. È possibile utilizzare cat big_file | tailo anche cat big_file | tee big_file.cpy | tailmolto spesso per questa velocità.
nategoose,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.