web-dev-qa-db-fra.com

Pourquoi l'impression sur stdout est-elle si lente? Peut-il être accéléré?

J'ai toujours été étonné/frustré par le temps nécessaire pour simplement imprimer sur le terminal avec une instruction print. Après une récente journalisation péniblement lente, j’ai décidé d’examiner cette question et étais assez surprise de constater que presque tout le temps passé attend que le terminal traite la résultats.

L'écriture sur stdout peut-elle être accélérée?

J'ai écrit un script ('print_timer.py 'au bas de cette question) pour comparer le timing lors de l'écriture de 100 000 lignes sur stdout, dans un fichier et avec stdout redirigé vers /dev/null. Voici le résultat du timing:

$ 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

Sensationnel. Pour vous assurer que python ne fait rien en coulisse, c'est-à-dire qu'il n'a pas reconnu que j'ai réaffecté stdout à/dev/null ou quelque chose d'autre, j'ai effectué la redirection en dehors du 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

Donc ce n’est pas un truc python, c’est juste le terminal. J'ai toujours su que le rendu de la sortie vers/dev/null accélérait les choses, mais je n’ai jamais pensé que c’était aussi important!

Je suis étonné de la lenteur du tty. Comment se fait-il que l'écriture sur un disque physique est bien plus rapide que l'écriture sur "l'écran" (vraisemblablement une opération tout en RAM) et qu'elle soit en réalité aussi rapide qu'un simple vidage à la poubelle avec/dev/null?

Ce lien explique comment le terminal bloquera les E/S afin qu'il puisse "analyser [l'entrée], mettre à jour son tampon de trame, communiquer avec le serveur X afin de faire défiler la fenêtre et donc " ... mais je ne comprends pas tout à fait. Que peut prendre si longtemps?

Je m'attends à ce qu'il n'y ait pas d'issue (à moins d'une implémentation plus rapide du tty?), Mais je demanderais quand même.


MISE À JOUR: après avoir lu certains commentaires, je me suis demandé quel impact ma taille d'écran a réellement sur le temps d'impression, et cela a une certaine signification. Les nombres très lents ci-dessus sont avec mon terminal Gnome gonflé à 1920x1200. Si je le réduis très petit je reçois ...

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

C'est certainement mieux (~ 4x), mais cela ne change pas ma question. Il ne fait que ajoute à ma question car je ne comprends pas pourquoi le rendu de l'écran du terminal devrait ralentir l'écriture d'une application sur stdout. Pourquoi mon programme doit-il attendre que le rendu de l'écran se poursuive?

Toutes les applications terminal/tty ne sont-elles pas créées égales? Je n'ai pas encore expérimenté. Il me semble vraiment qu'un terminal devrait pouvoir mettre en mémoire tampon toutes les données entrantes, les analyser/les restituer de manière invisible et ne restituer que le bloc le plus récent visible dans la configuration d'écran actuelle à une cadence raisonnable. Donc, si je peux écrire + fsync sur le disque en ~ 0,1 seconde, un terminal devrait pouvoir effectuer la même opération dans quelque chose de cet ordre (avec peut-être quelques mises à jour d'écran pendant qu'il le faisait).

J'espère toujours qu'il y a un paramètre tty qui peut être modifié du côté de l'application pour améliorer ce comportement pour le programmeur. S'il s'agit strictement d'un problème d'application de terminal, cela n'appartient peut-être même pas à StackOverflow?

Qu'est-ce que je rate?


Voici le programme python utilisé pour générer le minutage:

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
158
Russ

Merci pour tous vos commentaires! J'ai fini par y répondre moi-même avec votre aide. Il est sale de répondre à votre propre question, cependant.

Question 1: Pourquoi l’impression sur stdout est-elle lente?

Réponse: L'impression sur la sortie standard est pas intrinsèquement lente. C'est le terminal avec lequel vous travaillez qui est lent. Et cela n'a pratiquement rien à voir avec la mise en mémoire tampon des E/S côté application (par exemple: python). Voir ci-dessous.

Question 2: Peut-il être accéléré?

Réponse: Oui, c'est possible, mais apparemment pas du côté du programme (le côté qui effectue l'impression sur la sortie standard). Pour accélérer le processus, utilisez un émulateur de terminal différent plus rapide.

Explication...

J'ai essayé un programme de terminal 'léger' auto-décrit appelé wterm et obtenu de manière significative de meilleurs résultats. Vous trouverez ci-dessous le résultat de mon script de test (au bas de la question) lors de l'exécution de wterm à 1920x1200 sur le même système où l'option d'impression de base prenait 12 secondes à l'aide de gnome-terminal:

 ----- 
 résumé de la synchronisation (100k lignes chacune) 
 ----- 
 impression: 0,261 s 
 écrire dans le fichier ( + fsync): 0,110 s 
 imprimer avec stdout =/dev/null: 0,050 s 

0.26s est BEAUCOUP mieux que 12s! Je ne sais pas si wterm est plus intelligent en ce qui concerne la façon dont il affiche à l'écran selon les lignes de ma suggestion (rendre la queue "visible" à une cadence raisonnable), ou s'il "fait juste moins " que gnome-terminal. Pour les besoins de ma question, j'ai cependant la réponse. gnome-terminal est lent.

Donc - si vous avez un long script qui vous semble lent et qui rejette des quantités énormes de texte sur stdout ... essayez un autre terminal et voyez s'il est meilleur!

Notez que je tire assez au hasard wterm des référentiels Ubuntu/Debian. Ce lien pourrait être le même terminal, mais je ne suis pas sûr. Je n'ai testé aucun autre émulateur de terminal.


Mise à jour: Comme je devais me gratter, j'ai testé toute une pile d'autres émulateurs de terminaux avec le même script et le plein écran (1920x1200). Mes statistiques collectées manuellement sont ici:

 wterm 0.3s 
 aterm 0.3s 
 rxvt 0.3s 
 mrxvt 0.4. 
 konsole 0.6s 
 yakuake 0,7s 
 7s min. 
 xterm 9s 
 12s gnomes terminaux 
 xfce4s 12 terminaux 
 vala-terminaux 18s 
 xvt 48s 

Les temps enregistrés sont collectés manuellement, mais ils étaient assez cohérents. J'ai enregistré la meilleure valeur (ish). YMMV, évidemment.

En prime, ce fut une visite intéressante de quelques-uns des différents émulateurs de terminaux disponibles! Je suis étonné que mon premier test "alternatif" se soit avéré être le meilleur du groupe.

84
Russ

Comment se fait-il que l'écriture sur un disque physique est bien plus rapide que l'écriture sur "l'écran" (vraisemblablement une opération tout en RAM) et qu'elle soit en réalité aussi rapide qu'un simple vidage à la poubelle avec/dev/null?

Félicitations, vous venez de découvrir l’importance de la mise en mémoire tampon des E/S. :-)

Le disque semble être plus rapide , car il est fortement mis en mémoire tampon: tous les appels write() de Python reviennent avant que quoi que ce soit ne soit réellement écrit en physique. disque. (Le système d'exploitation le fait plus tard, combinant plusieurs milliers d'écritures individuelles en un gros morceau efficace.)

Le terminal, d’autre part, met peu ou pas en mémoire tampon: chaque personne print/write(line) attend l’écriture complète (affichage sur le périphérique de sortie) dans Achevée.

Pour que la comparaison soit équitable, vous devez faire en sorte que le test de fichier utilise la même mise en mémoire tampon de sortie que le terminal, ce que vous pouvez faire en modifiant votre exemple pour:

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

J'ai exécuté votre test d'écriture de fichier sur ma machine, et avec la mise en mémoire tampon, il a également une valeur de 0,05 seconde pour 100 000 lignes.

Cependant, avec les modifications ci-dessus pour écrire sans tampon, il faut 40 secondes pour écrire seulement 1 000 lignes sur le disque. J'ai arrêté d'attendre 100 000 lignes pour écrire, mais en extrapolant à partir de la précédente, cela prendrait plus d'une heure.

Cela met les 11 secondes du terminal en perspective, n'est-ce pas?

Donc, pour répondre à votre question initiale, écrire sur un terminal est extrêmement rapide, tout compte fait, et il n’ya pas beaucoup de place pour le faire beaucoup plus rapidement (mais le travail de chaque terminal varie beaucoup; voir le commentaire de Russ à ce sujet. répondre).

(Vous pouvez ajouter davantage de mémoire tampon en écriture, comme avec les E/S de disque, mais vous ne verrez pas ce qui a été écrit sur votre terminal tant que la mémoire tampon n'aura pas été vidée. C'est un compromis: interactivité versus efficacité globale.)

146
Pi Delport

Votre redirection ne fait probablement rien car les programmes peuvent déterminer si leur FD en sortie pointe vers un terminal.

Il est probable que stdout soit doté d'une mémoire tampon en ligne lors du pointage vers un terminal (identique à celui de C stdout comportement du flux).

Comme expérience amusante, essayez d’attribuer la sortie à cat.


J'ai essayé ma propre expérience amusante et voici les résultats.

$ 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
Hasturkun

Je ne peux pas parler des détails techniques car je ne les connais pas, mais cela ne me surprend pas: le terminal n'a pas été conçu pour imprimer beaucoup de données comme celle-ci. En effet, vous fournissez même un lien vers un tas de choses que vous devez faire chaque fois que vous voulez imprimer! Notez que si vous appelez le script avec pythonw à la place, cela ne prend pas 15 secondes; c'est entièrement un problème d'interface graphique. Rediriger stdout vers un fichier pour éviter ceci:

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
Katriel

L'impression sur le terminal va être lente. Malheureusement, à moins d’écrire une nouvelle implémentation de terminal, je ne vois pas vraiment comment vous pourriez l’accélérer de manière significative.

3
shuttle87

En plus de la sortie probablement par défaut du mode ligne tamponné, la sortie vers un terminal entraîne également le flux de vos données dans un terminal et une ligne série avec un débit maximal, ou un pseudo-terminal et un processus séparé qui gère un affichage. boucle d’événements, restituant les caractères d’une police, déplaçant les bits d’affichage pour implémenter un affichage défilant. Ce dernier scénario est probablement réparti sur plusieurs processus (par exemple, serveur/client telnet, application de terminal, serveur d'affichage X11), de sorte qu'il existe également des problèmes de commutation de contexte et de latence.

2
Liudvikas Bukys