Wie schreibt "Ja" so schnell in eine Datei?

58

Lassen Sie mich ein Beispiel geben:

$ 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

Hier können Sie sehen, dass der Befehl Zeilen in einer Sekunde yesschreibt 11504640, während ich 1953mit bashs forund nur Zeilen in 5 Sekunden schreiben kann echo.

Wie in den Kommentaren vorgeschlagen, gibt es verschiedene Tricks, um die Effizienz zu steigern, aber keine entspricht in etwa der Geschwindigkeit von 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

Diese können bis zu 20.000 Zeilen pro Sekunde schreiben. Und sie können weiter verbessert werden, um:

$ 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

Diese bringen uns in einer Sekunde bis zu 40.000 Zeilen. Besser, aber noch weit entfernt, mit yesdem man in einer Sekunde ungefähr 11 Millionen Zeilen schreiben kann!

Also, wie yesschreibt man so schnell in eine Datei?

Pandya
quelle
9
Im zweiten Beispiel haben Sie zwei externe Befehlsaufrufe für jede Iteration der Schleife und datesind etwas schwergewichtig. Außerdem muss die Shell den Ausgabestream echofür jede Schleifeniteration erneut öffnen . Im ersten Beispiel gibt es nur einen einzigen Befehlsaufruf mit einer einzigen Ausgabeumleitung, und der Befehl ist äußerst kompakt. Die beiden sind in keiner Weise vergleichbar.
ein Lebenslauf am
@ MichaelKjörling du hast recht datekann schwergewichtig sein, siehe meine frage bearbeiten.
Pandya
1
timeout 1 $(while true; do echo "GNU">>file2; done;)ist die falsche Methode, timeout da der timeoutBefehl erst gestartet wird, wenn die Befehlsersetzung abgeschlossen ist. Verwenden Sie timeout 1 sh -c 'while true; do echo "GNU">>file2; done'.
Muru
1
Zusammenfassung der Antworten: write(2)Indem Sie in Ihrem ersten Beispiel nur CPU-Zeit für Systemaufrufe verwenden, nicht für Bootloads anderer Systemaufrufe, Shell-Overhead oder sogar für die Erstellung von Prozessen (die ausgeführt werden und auf datejede Zeile warten, die in der Datei gedruckt wird). Eine Sekunde des Schreibens reicht kaum aus, um auf einem modernen System mit viel RAM einen Engpass bei der Datenträger-E / A (anstelle von CPU / Speicher) zu verursachen. Wenn länger laufen darf, wäre der Unterschied geringer. (Abhängig davon, wie schlecht eine Bash-Implementierung ist und wie schnell die CPU und der Datenträger sind, können Sie die Datenträger-E / A mit Bash möglicherweise nicht einmal auslasten.)
Peter Cordes

Antworten:

65

Nussschale:

yeszeigt ein ähnliches Verhalten wie die meisten anderen Standarddienstprogramme, die normalerweise in einen FILE STREAM schreiben , wobei die Ausgabe von der libC über stdio gepuffert wird . Diese rufen nur alle 4 kb (16 kb oder 64 kb) oder unabhängig vom Ausgangsblock BUFSIZ das System auf . ist ein pro . Das ist eine Menge von Modus-Umschaltung (das ist anscheinend nicht so teuer , wie ein Kontext-Switch ) .write()echowrite()GNU

Und das ist überhaupt nicht zu erwähnen, dass es sich neben der anfänglichen Optimierungsschleife yesum eine sehr einfache, winzige, kompilierte C-Schleife handelt und Ihre Shell-Schleife in keiner Weise mit einem vom Compiler optimierten Programm vergleichbar ist.


aber ich habe mich getäuscht:

Als ich vorher sagte, dass yesstdio verwendet wird, habe ich nur angenommen, dass dies der Fall ist, weil es sich sehr ähnlich verhält wie diejenigen, die dies tun. Dies war nicht korrekt - es emuliert nur ihr Verhalten auf diese Weise. Was es tatsächlich tut, ist sehr ähnlich wie das, was ich unten mit der Shell gemacht habe: Es schleift zuerst, um seine Argumente (oder ywenn keine vorhanden sind) zu verknüpfen, bis sie nicht mehr wachsen, ohne zu überschreiten BUFSIZ.

Ein Kommentar aus der Quelle unmittelbar vor der betreffenden forSchleife lautet:

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

yesmacht sein macht sein eigenes write()s danach.


Abschweifung:

(Wie ursprünglich in der Frage enthalten und für den Kontext zu einer möglicherweise informativen Erklärung beibehalten, die bereits hier geschrieben wurde) :

Ich habe versucht, timeout 1 $(while true; do echo "GNU">>file2; done;)aber nicht in der Lage, die Schleife zu stoppen.

Das timeoutProblem, das Sie mit der Befehlsersetzung haben - ich glaube, ich verstehe es jetzt und kann erklären, warum es nicht aufhört. timeoutstartet nicht, weil die Kommandozeile nie ausgeführt wird. Ihre Shell gabelt eine untergeordnete Shell, öffnet eine Pipe auf ihrer Standardausgabe und liest sie. Es hört auf zu lesen, wenn das Kind aufhört, und interpretiert dann alles, was das Kind für $IFSMangeln und Glob-Erweiterungen geschrieben hat, und ersetzt mit den Ergebnissen alles von $(bis zum Matching ).

Aber wenn das Kind eine Endlosschleife, die nie an das Rohr schreibt, dann nie das Kind hält Looping und timeout‚s - Befehlszeile nie zuvor abgeschlossen ist (wie ich denke) Sie tun CTRL-Cund das Kind Schleife töten. So timeoutkann niemals die Schleife töten , die abgeschlossen werden muss , bevor es gestartet werden kann.


andere timeouts:

... sind für Ihre Performance-Probleme einfach nicht so relevant wie die Zeit, die Ihr Shell-Programm für den Wechsel zwischen Benutzer- und Kernel-Modus benötigt, um die Ausgabe zu erledigen. timeoutEs ist jedoch nicht so flexibel wie eine Shell für diesen Zweck: Wo Shells Excel in der Lage ist, Argumente zu entstellen und andere Prozesse zu verwalten.

Wie an anderer Stelle erwähnt, [fd-num] >> named_filekann die Leistung erheblich verbessert werden, wenn Sie Ihre Umleitung einfach auf das Ausgabeziel der Schleife verschieben, anstatt nur die Ausgabe für den übergebenen Befehl dorthin zu leiten, da auf diese Weise zumindest der open()Systemaufruf nur einmal ausgeführt werden muss. Dies geschieht auch weiter unten mit dem |Rohr, das als Ausgang für die inneren Schleifen vorgesehen ist.


direkter Vergleich:

Das könnte Ihnen gefallen:

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

Das ist Art von wie die Befehlsunter Beziehung zuvor beschrieben, aber es gibt keine Pfeife und das Kind backgrounded, bis sie die Eltern töten. In dem yesFall, dass der Elternteil tatsächlich ersetzt wurde, seit das Kind erzeugt wurde, aber die Shell ruft auf, yesindem sie ihren eigenen Prozess mit dem neuen überlagert, sodass die PID gleich bleibt und das Zombie-Kind immer noch weiß, wen es töten soll.


größerer Puffer:

Nun sehen wir uns an, wie der write()Puffer der Shell erhöht wird .

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

Ich habe diese Zahl gewählt, weil die Ausgabe-Strings, die länger als 1 KB sind, write()für mich in separate aufgeteilt wurden . Und hier ist nochmal die Schleife:

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

Das ist das 300-fache der Datenmenge, die von der Shell in derselben Zeit für diesen Test geschrieben wurde wie für den letzten. Nicht zu schäbig. Ist es aber nicht yes.


verbunden:

Wie gewünscht, gibt es eine ausführlichere Beschreibung als die bloßen Code-Kommentare zu dem, was hier unter diesem Link gemacht wird .

mikeserv
quelle
@heemayl - vielleicht? Ich bin nicht ganz sicher, ob ich verstehe, was du fragst? Wenn ein Programm stdio zum Schreiben von Ausgaben verwendet, erfolgt dies entweder ohne Pufferung (wie standardmäßig stderr) oder Zeilenpufferung (standardmäßig Terminals) oder Blockpufferung (im Grunde sind die meisten anderen Dinge standardmäßig so eingestellt) . Ich bin ein wenig unklar, was die Größe des Ausgabepuffers festlegt - aber normalerweise sind es 4 KB. und so sammeln die stdio lib-funktionen ihre ausgaben, bis sie einen ganzen block schreiben können. ddist ein Standardwerkzeug, das zum Beispiel definitiv kein stdio verwendet. die meisten anderen tun es.
MikeServ
3
Die Shell-Version macht open(existierendes) writeAND close(was meiner Meinung nach immer noch auf Flush wartet) UND erstellt einen neuen Prozess und dateführt ihn für jede Schleife aus.
Dave_thompson_085
@ dave_thompson_085 - gehe zu / dev / chat . und was Sie sagen, ist nicht unbedingt wahr, wie Sie dort sehen können. Zum Beispiel, wenn ich diese wc -lSchleife mit mache, bekomme ich ein Fünftel bashder Ausgabe, die die shSchleife macht - bashverwaltet etwas mehr als 100k writes()bis dashzu 500k.
MikeServ
Tut mir leid, ich war mehrdeutig; Ich meinte die Shell-Version in der Frage, die zum Zeitpunkt meiner Lektüre nur die Originalversion mit der for((sec0=`date +%S`;...zu steuernden Zeit und der Umleitung in der Schleife hatte, nicht die nachträglichen Verbesserungen.
Dave_thompson_085
@ Dave_thompson_085 - es ist in Ordnung. Die Antwort war in einigen grundsätzlichen Punkten ohnehin falsch und sollte, wie ich hoffe, jetzt ziemlich richtig sein.
mikeserv
20

Eine bessere Frage wäre, warum Ihre Shell die Datei so langsam schreibt. Jedes in sich geschlossene kompilierte Programm, das Systemaufrufe zum Schreiben von Dateien verantwortungsbewusst ausführt (nicht jedes Zeichen gleichzeitig löscht), würde dies relativ schnell tun. Was Sie tun, ist das Schreiben von Zeilen in einer interpretierten Sprache (der Shell), und außerdem führen Sie eine Menge unnötiger Eingabe- / Ausgabeoperationen aus. Was yesmacht:

  • öffnet eine Datei zum Schreiben
  • ruft optimierte und kompilierte Funktionen zum Schreiben in einen Stream auf
  • Der Stream ist gepuffert, so dass ein Syscall (ein teurer Wechsel in den Kernel-Modus) in großen Blöcken sehr selten vorkommt
  • Schließt eine Datei

Was Ihr Skript macht:

  • liest eine Codezeile ein
  • interpretiert den Code und führt eine Menge zusätzlicher Operationen durch, um Ihre Eingaben tatsächlich zu analysieren und herauszufinden, was zu tun ist
  • für jede Iteration der while-Schleife (was in einer interpretierten Sprache wahrscheinlich nicht billig ist):
    • Rufen Sie das dateexterne Kommando auf und speichern Sie dessen Ausgabe (nur in der Originalversion - in der überarbeiteten Version erhalten Sie den Faktor 10, wenn Sie dies nicht tun)
    • Test, ob die Abbruchbedingung der Schleife erfüllt ist
    • Öffnen Sie eine Datei im Anhänge-Modus
    • parse echoKommando, erkenne es (mit einigem Pattern Matching Code) als eine eingebaute Shell, rufe die Parametererweiterung und alles andere für das Argument "GNU" auf und schreibe schließlich die Zeile in die geöffnete Datei
    • Schließen Sie die Datei erneut
    • Wiederholen Sie den Vorgang

Die teuren Teile: Die gesamte Interpretation ist extrem teuer (Bash macht eine Menge Vorverarbeitung aller Eingaben - Ihre Zeichenkette könnte möglicherweise variable Substitution, Prozesssubstitution, Klammererweiterung, Escape-Zeichen und mehr enthalten), jeder Aufruf eines eingebauten Befehls ist wahrscheinlich eine switch-Anweisung mit Weiterleitung zu einer Funktion, die sich mit dem eingebauten Code befasst, und vor allem öffnen und schließen Sie eine Datei für jede einzelne Ausgabezeile. Sie können >> filedie while-Schleife auch außerhalb der Schleife platzieren, um den Vorgang zu beschleunigen , aber Sie sprechen immer noch eine interpretierte Sprache. Sie haben das große Glückechoist eine eingebaute Shell, kein externer Befehl - andernfalls müsste in Ihrer Schleife bei jeder einzelnen Iteration ein neuer Prozess (fork & exec) erstellt werden. Was den Prozess zum Stillstand bringen würde - Sie haben gesehen, wie kostspielig das war, als Sie den dateBefehl in der Schleife hatten.

orion
quelle
11

Die anderen Antworten haben die Hauptpunkte angesprochen. Nebenbei bemerkt, Sie können den Durchsatz Ihrer while-Schleife erhöhen, indem Sie am Ende der Berechnung in die Ausgabedatei schreiben. Vergleichen Sie:

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

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

mit

$ 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
quelle
Ja, das ist wichtig und die Schreibgeschwindigkeit verdoppelt sich in meinem Fall (mindestens)
Pandya