Comment `oui` écrit-il dans un fichier si rapidement?

58

Laissez-moi vous donner un exemple:

$ 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

Ici vous pouvez voir que la commande yesécrit des 11504640lignes en une seconde alors que je ne peux écrire que des 1953lignes en 5 secondes en utilisant bash foret echo.

Comme suggéré dans les commentaires, il existe différentes astuces pour le rendre plus efficace, mais aucune ne correspond à la vitesse 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

Ceux-ci peuvent écrire jusqu'à 20 000 lignes en une seconde. Et ils peuvent être encore améliorés pour:

$ 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

Cela nous amène à 40 000 lignes en une seconde. Mieux, mais loin de là, yeson peut écrire environ 11 millions de lignes en une seconde!

Alors, comment yesécrire dans un fichier si rapidement?

Pandya
la source
9
Dans le deuxième exemple, vous avez deux invocations de commandes externes pour chaque itération de la boucle et son datepoids est assez lourd. En outre, le shell doit rouvrir le flux de sortie echopour chaque itération de la boucle. Dans le premier exemple, il n'y a qu'une seule invocation de commande avec une seule redirection de sortie, et la commande est extrêmement légère. Les deux ne sont nullement comparables.
un CVn
@ MichaelKjörling vous avez raison datepeut être lourd, voir modifier à ma question.
Pandya
1
timeout 1 $(while true; do echo "GNU">>file2; done;)est la mauvaise façon d'utiliser timeout car la timeoutcommande ne démarrera qu'une fois la substitution de commande terminée. Utilisez timeout 1 sh -c 'while true; do echo "GNU">>file2; done'.
Muru
1
résumé des réponses: en ne consacrant que votre temps processeur aux write(2)appels système, et non aux charges de bateaux d'autres appels système, à la surcharge du shell ou même à la création de processus dans votre tout premier exemple (qui s'exécute et attend datepour chaque ligne imprimée dans le fichier). Une seconde d’écriture suffit à peine à goulot d’étranglement sur les E / S du disque (plutôt que sur le processeur / la mémoire), sur un système moderne disposant de beaucoup de RAM. Si on laisse courir plus longtemps, la différence serait moins grande. (En fonction de la qualité de votre implémentation bash et de la vitesse relative du processeur et du disque, il est possible que vous ne saturiez même pas les E / S du disque avec bash).
Peter Cordes

Réponses:

65

coquille de noix:

yesprésente un comportement similaire à la plupart des autres utilitaires standard qui écrivent généralement dans un FILE STREAM avec la sortie tamponnée par la bibliothèque libC via stdio . Ceux-ci ne font que l'appel système write()tous les 4 ko (16 ko ou 64 ko) ou quel que soit le bloc de sortie BUFSIZ . echoest un write()per GNU. C'est beaucoup de changement de mode (ce qui, apparemment, n'est pas aussi coûteux qu'un changement de contexte ) .

Et cela ne yesveut pas du tout dire que, outre sa boucle d’optimisation initiale, il s’agit d’une très simple, petite boucle C compilée et que votre boucle shell n’est en aucun cas comparable à un programme optimisé pour le compilateur.


Mais je me trompais:

Quand j'ai dit auparavant que yesstdio était utilisé, je pensais que c'était le cas, car il se comporte beaucoup comme ceux qui le font. Ce n'était pas correct - cela ne fait que simuler leur comportement. Ce qu’il fait en réalité ressemble beaucoup à ce que j’ai fait ci-dessous avec le shell: il commence par boucler la fusion de ses arguments (ou ys’il n’en a pas un) jusqu’à ce qu’ils ne puissent plus grandir sans dépasser BUFSIZ.

Un commentaire de la source précédant immédiatement la forboucle en question indique:

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

yesfait sa fait sa propre write()s par la suite.


digression:

(Tel qu'initialement inclus dans la question et retenu comme contexte pour une explication éventuellement informative déjà écrite ici) :

J'ai essayé timeout 1 $(while true; do echo "GNU">>file2; done;)mais impossible d'arrêter la boucle.

Le timeoutproblème que vous avez avec la substitution de commande - je pense que je l’ai compris maintenant, et peut expliquer pourquoi cela ne s’arrête pas. timeoutne démarre pas car sa ligne de commande n'est jamais exécutée. Votre shell fourche un shell enfant, ouvre un tuyau sur sa sortie standard et le lit. Il arrêtera de lire lorsque l'enfant quittera, puis interprétera tout ce qu'il a écrit pour les $IFSextensions globales et globales, et avec les résultats, il remplacera tout, de $(la correspondance ).

Mais si l'enfant est une boucle sans fin qui n'a jamais écrit à la conduite, l'enfant ne cesse jamais en boucle, et timeoutest jamais terminé commande en ligne de » avant (que je suppose) que vous faites CTRL-Cet tuer la boucle de l' enfant. Donc, netimeout peut jamais tuer la boucle qui doit se terminer avant de pouvoir commencer.


autres timeouts:

... ne sont tout simplement pas aussi pertinents pour vos problèmes de performances que le temps que votre programme shell doit passer de la commutation entre le mode utilisateur et le mode noyau pour gérer la sortie. timeoutCependant, n’est pas aussi souple qu’un shell peut l’être à cette fin: lorsque les shells excellent, c’est leur capacité à gérer les arguments et à gérer d’autres processus.

Comme indiqué ailleurs, le simple fait de déplacer votre [fd-num] >> named_fileredirection vers la cible de sortie de la boucle plutôt que d'y diriger uniquement la sortie pour la commande en boucle peut considérablement améliorer les performances, car au moins le open()syscall ne doit être exécuté qu'une seule fois. Ceci est également fait ci-dessous avec le |tuyau ciblé comme sortie pour les boucles internes.


comparaison directe:

Vous pourriez faire comme:

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

Ce qui est un peu comme la relation de commande décrite précédemment, mais il n'y a pas de canal et l'enfant est en arrière-plan jusqu'à ce qu'il tue le parent. Dans le yescas où le parent a effectivement été remplacé depuis la génération de l'enfant, mais le shell appelle yesen superposant son propre processus avec le nouveau et le PID reste le même et son enfant zombie sait toujours qui tuer après tout.


plus grand tampon:

Voyons maintenant comment augmenter la write()mémoire tampon du 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

J'ai choisi ce nombre parce que les chaînes de sortie de plus de 1 Ko étaient séparées write()pour moi. Et voici donc à nouveau la boucle:

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

Cela représente 300 fois la quantité de données écrites par le shell dans le même laps de temps pour ce test que pour le dernier. Pas trop mal. Mais ce n'est pas yes.


apparenté, relié, connexe:

Comme demandé, il existe une description plus détaillée que les simples commentaires de code sur ce qui est fait ici sur ce lien .

Mikeserv
la source
@heemayl - peut-être? je ne suis pas tout à fait sûr de comprendre ce que vous demandez? Lorsqu'un programme utilise stdio pour écrire la sortie, il le fait sans tampon (comme stderr par défaut) ou en tampon de ligne (vers les terminaux par défaut) ou en tampon (essentiellement la plupart des autres éléments sont définis de cette manière) . Je ne sais pas trop ce qui définit la taille de la mémoire tampon de sortie - mais il s’agit généralement de 4 kb. ainsi, les fonctions de bibliothèque stdio rassembleront leur sortie jusqu'à ce qu'elles puissent écrire un bloc entier. ddest un outil standard qui n'utilise certainement pas stdio, par exemple. la plupart des autres le font.
mikeserv
3
La version shell est en train de faire open(exister) writeAND close(ce qui, je crois, attend toujours le flush), ET de créer un nouveau processus et de l'exécuter date, pour chaque boucle.
dave_thompson_085
@ dave_thompson_085 - allez à / dev / chat . et ce que vous dites n'est pas nécessairement vrai, comme vous pouvez le voir là-bas. Par exemple, faire cette wc -lboucle avec bashmoi obtient 1 / 5ème de la sortie de la shboucle - bashgère un peu plus de 100k writes()à dash500k.
mikeserv
Désolé j'étais ambigu; Je parlais de la version shell de la question qui, à la lecture du livre, ne comportait que la version originale avec le for((sec0=`date +%S`;...contrôle de l'heure et la redirection dans la boucle, et non les améliorations ultérieures.
dave_thompson_085
@ dave_thompson_085 - c'est bien. la réponse était de toute façon fausse sur certains points fondamentaux et devrait être à peu près correcte maintenant, comme je l’espère.
mikeserv
20

Une meilleure question serait de savoir pourquoi votre shell écrit le fichier si lentement. Tout programme compilé autonome qui utilise les appels système de manière responsable (sans effacer tous les caractères à la fois) le ferait raisonnablement rapidement. Ce que vous faites, c'est écrire des lignes dans un langage interprété (le shell), et en plus, vous effectuez beaucoup d'opérations de sortie d'entrée inutiles. Qu'est yes- ce que:

  • ouvre un fichier pour l'écriture
  • appelle des fonctions optimisées et compilées pour écrire dans un flux
  • le flux est mis en mémoire tampon, donc un appel système (une commutation coûteuse en mode noyau) se produit très rarement, par gros morceaux
  • ferme un fichier

Qu'est-ce que votre script fait:

  • lit dans une ligne de code
  • interprète le code, en effectuant de nombreuses opérations supplémentaires pour analyser vos entrées et déterminer ce qu'il faut faire
  • pour chaque itération de la boucle while (ce qui n'est probablement pas bon marché dans un langage interprété):
    • appelez la datecommande externe et stockez sa sortie (uniquement dans la version d'origine - dans la version révisée, vous obtenez un facteur 10 en ne le faisant pas)
    • tester si la condition de terminaison de la boucle est remplie
    • ouvrir un fichier en mode ajout
    • analyser la echocommande, la reconnaître (avec un code de correspondance de modèle) comme un shell intégré, appeler le développement des paramètres et tout le reste sur l'argument "GNU", et enfin écrire la ligne dans le fichier ouvert
    • refermer le fichier
    • répéter le processus

Les parties coûteuses: toute l’interprétation est extrêmement coûteuse (bash effectue énormément de pré-traitement de toutes les entrées - votre chaîne pourrait potentiellement contenir une substitution de variable, une substitution de processus, une extension d’accolade, des caractères d’échappement, etc.), chaque appel d’une commande intégrée est probablement une instruction switch avec redirection vers une fonction qui traite de la fonction intégrée, et très important, vous ouvrez et fermez un fichier pour chaque ligne de sortie. Vous pourriez mettre >> filehors de la boucle while pour le rendre beaucoup plus rapide , mais vous êtes toujours dans un langage interprété. Vous avez de la chanceechoest un shell intégré, pas une commande externe - sinon, votre boucle impliquerait la création d'un nouveau processus (fork & exec) à chaque itération. Ce qui aurait paralysé le processus - vous avez vu à quel point c'était coûteux lorsque vous aviez la datecommande dans la boucle.

orion
la source
11

Les autres réponses ont abordé les points principaux. En passant, vous pouvez augmenter le débit de votre boucle while en écrivant dans le fichier de sortie à la fin du calcul. Comparer:

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

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

avec

$ 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
la source
Oui, cela compte et la vitesse d'écriture (au moins) double dans mon cas
Pandya