Die Beantwortung dieser Frage veranlasste mich, eine andere Frage zu stellen:
Ich dachte, die folgenden Skripte tun dasselbe und die zweite sollte viel schneller sein, da die erste verwendet cat
, die die Datei immer wieder öffnen muss, aber die zweite nur die Datei öffnet einmal und gibt dann nur eine Variable aus:
(Den richtigen Code finden Sie im Update-Abschnitt.)
Zuerst:
#!/bin/sh
for j in seq 10; do
cat input
done >> output
Zweite:
#!/bin/sh
i=`cat input`
for j in seq 10; do
echo $i
done >> output
während die Eingabe etwa 50 Megabyte beträgt.
Aber als ich das zweite ausprobierte, war es zu langsam, weil es die Variable wiedergab i
es ein gewaltiger Prozess . Ich habe auch einige Probleme mit dem zweiten Skript, zum Beispiel die Größe der Ausgabedatei war geringer als erwartet.
Ich habe auch die Manpage von echo
und durchgesehencat
sie verglichen:
echo - Zeigt eine Textzeile an
cat - Dateien verketten und auf der Standardausgabe drucken
Aber ich habe den Unterschied nicht verstanden.
So:
- Warum ist die Katze so schnell und das Echo im zweiten Drehbuch so langsam?
- Oder ist das Problem mit der Variablen
i
? (Weil in der Manpageecho
davon die Rede ist, dass es "eine Textzeile" anzeigt, und ich vermute, dass es nur für kurze Variablen optimiert ist, nicht für sehr lange Variablen wiei
. Dies ist jedoch nur eine Vermutung.) - Und warum habe ich Probleme, wenn ich benutze
echo
?
AKTUALISIEREN
Ich habe seq 10
statt `seq 10`
falsch verwendet. Dies ist bearbeiteter Code:
Zuerst:
#!/bin/sh
for j in `seq 10`; do
cat input
done >> output
Zweite:
#!/bin/sh
i=`cat input`
for j in `seq 10`; do
echo $i
done >> output
(Besonderer Dank geht an Roaima .)
Es ist jedoch nicht der Punkt des Problems. Auch wenn die Schleife nur einmal auftritt, bekomme ich das gleiche Problem: cat
Arbeitet viel schneller als echo
.
cat $(for i in $(seq 1 10); do echo "input"; done) >> output
? :)echo
geht schneller. Was Sie vermissen, ist, dass Sie die Shell dazu bringen, viel zu viel Arbeit zu leisten, indem Sie die Variablen nicht in Anführungszeichen setzen, wenn Sie sie verwenden.printf '%s' "$i"
, nichtecho $i
. @cuonglm erklärt einige der Echoprobleme gut in seiner Antwort. Warum in einigen Fällen mit Echo nicht einmal das Zitieren ausreicht, erfahrenAntworten:
Hier sind einige Dinge zu beachten.
kann teuer sein und es gibt viele Variationen zwischen den Schalen.
Dies ist eine Funktion, die als Befehlsersetzung bezeichnet wird. Die Idee ist, die gesamte Ausgabe des Befehls abzüglich der nachfolgenden Zeilenumbrüche in der
i
Variablen im Speicher zu speichern.Dazu geben Shells den Befehl in einer Subshell an und lesen seine Ausgabe über eine Pipe oder ein Socket-Paar. Sie sehen hier eine Menge Variationen. In einer 50-MB-Datei sehe ich beispielsweise, dass Bash 6-mal so langsam ist wie ksh93, aber etwas schneller als zsh und doppelt so schnell wie
yash
.Der Hauptgrund dafür
bash
, dass es langsam ist, ist, dass es 128 Bytes gleichzeitig aus der Pipe liest (während andere Shells 4 KB oder 8 KB gleichzeitig lesen) und durch den Systemaufruf-Overhead bestraft wird.zsh
Nachbearbeitung erforderlich, um NUL-Bytes zu umgehen (andere Shells setzen NUL-Bytes außer Kraft), undyash
noch umfangreichere Nachbearbeitung durch Analysieren von Multi-Byte-Zeichen.Alle Shells müssen die nachfolgenden Zeilenumbrüche entfernen, was sie möglicherweise mehr oder weniger effizient ausführen.
Einige möchten möglicherweise mit NUL-Bytes eleganter umgehen als andere und auf Vorhandensein prüfen.
Sobald Sie diese große Variable im Arbeitsspeicher haben, müssen Sie bei jeder Manipulation im Allgemeinen mehr Arbeitsspeicher zuweisen und Daten übertragen.
Hier übergeben Sie (wollten) den Inhalt der Variablen an
echo
.Zum Glück
echo
ist es in Ihrer Shell eingebaut, sonst wäre die Ausführung wahrscheinlich mit einem zu langen Fehler fehlgeschlagen . Selbst dann wird das Erstellen des Argumentlistenarrays möglicherweise das Kopieren des Inhalts der Variablen umfassen.Das andere Hauptproblem bei Ihrem Befehlssubstitutionsansatz besteht darin, dass Sie den Operator split + glob aufrufen (indem Sie vergessen, die Variable in Anführungszeichen zu setzen).
Dazu Schalen müssen die Zeichenfolge als eine Folge von behandeln Zeichen (obwohl einige Granaten nicht und sind fehlerhaft in dieser Hinsicht) so in UTF-8 - Locales, dass Mittel UTF-8 - Sequenzen Parsen (falls noch nicht geschehen , wie der
yash
Fall ist) Suchen Sie nach$IFS
Zeichen in der Zeichenfolge. Wenn$IFS
Leerzeichen, Tabulatoren oder Zeilenumbrüche enthalten sind (was standardmäßig der Fall ist), ist der Algorithmus noch komplexer und teurer. Dann müssen die aus dieser Aufteilung resultierenden Wörter zugewiesen und kopiert werden.Der Glob-Teil wird noch teurer. Wenn eines dieser Wörter glob Zeichen (
*
,?
,[
), dann wird die Shell den Inhalt einiger Verzeichnisse lesen und einige teure Musterabgleich tun (bash
s Implementierung zum Beispiel ist bekanntlich sehr schlecht dazu).Wenn die Eingabe so etwas enthält
/*/*/*/../../../*/*/*/../../../*/*/*
, ist das extrem teuer, da das bedeutet, dass Tausende von Verzeichnissen aufgelistet werden und sich diese auf mehrere Hundert MiB erweitern können.Dann
echo
wird in der Regel eine zusätzliche Verarbeitung durchgeführt. Einige Implementierungen erweitern\x
Sequenzen in dem Argument, das sie erhalten, was bedeutet, dass der Inhalt und wahrscheinlich eine andere Zuordnung und Kopie der Daten analysiert werden.Auf der anderen Seite ist OK in den meisten Shells
cat
nicht integriert. Dies bedeutet, dass Sie einen Prozess forken und ausführen (also den Code und die Bibliotheken laden), aber nach dem ersten Aufruf diesen Code und den Inhalt der Eingabedatei wird im Speicher zwischengespeichert. Auf der anderen Seite wird es keinen Vermittler geben.cat
liest große Mengen gleichzeitig und schreibt sie sofort ohne Verarbeitung, und es muss keine große Menge an Speicher reserviert werden, nur der eine Puffer, den es wiederverwendet.Dies bedeutet auch, dass es viel zuverlässiger ist, da es nicht an NUL-Bytes verstopft und nachfolgende Zeilenumbrüche nicht abschneidet Erweitern Sie die Escape-Sequenz, obwohl Sie dies vermeiden können, indem Sie
printf
anstelle vonecho
) verwenden.Wenn Sie es weiter optimieren möchten, anstatt es
cat
mehrmals aufzurufen , übergeben Sie es einfachinput
mehrmals ancat
.Führt 3 anstelle von 100 Befehlen aus.
Um die variable Version zuverlässiger zu machen, müssen Sie Folgendes verwenden
zsh
(andere Shells können mit NUL-Bytes nicht umgehen):Wenn Sie wissen, dass die Eingabe keine NUL-Bytes enthält, können Sie dies POSIX-zuverlässig tun (obwohl dies möglicherweise nicht funktioniert, wenn
printf
keine eingebauten Daten vorliegen) mit:Aber das wird niemals effizienter als die Verwendung
cat
in der Schleife (es sei denn, die Eingabe ist sehr klein).quelle
/bin/echo $(perl -e 'print "A"x999999')
dd bs=128 < input > /dev/null
mitdd bs=64 < input > /dev/null
. Von den 0,6s, die zum Lesen dieser Dateiread
erforderlich sind, werden 0,4 in diesen Systemaufrufen in meinen Tests ausgegeben , während andere Shells viel weniger Zeit dort verbringen.readwc()
undtrim()
in der Burne Shell 30% der gesamten Zeit in Anspruch nehmen, und dies wird höchstwahrscheinlich unterschätzt, da es keine libc mitgprof
Annotation für gibtmbtowc()
.\x
erweitert?Das Problem ist nicht etwa
cat
undecho
, es geht um das Zitat Variable vergessen$i
.zsh
Wenn Sie in einem Bourne-ähnlichen Shell-Skript (außer ) Variablen nicht in Anführungszeichen setzen, werden die Variablen vonglob+split
Operatoren bearbeitet .ist eigentlich:
So wird bei jeder Schleifeniteration der gesamte Inhalt von
input
(ohne nachfolgende Zeilenumbrüche) erweitert, aufgeteilt und verschoben. Für den gesamten Prozess muss die Shell Speicher zuweisen und die Zeichenfolge immer wieder analysieren. Das ist der Grund, warum du die schlechte Leistung hast.Sie können die Variable in Anführungszeichen setzen, um
glob+split
dies zu verhindern, aber es wird Ihnen nicht viel helfen, da die Shell immer noch das große Zeichenfolgenargument erstellen und dessen Inhalt nachecho
(Ersetzen von builtinecho
durch external/bin/echo
führt dazu, dass die Argumentliste zu lang ist oder nicht genügend Speicherplatz zur Verfügung steht abhängig von der$i
Größe). Die meistenecho
Implementierungen sind nicht POSIX-konform. Sie erweitern die Backslash-\x
Sequenzen in den empfangenen Argumenten.Mit
cat
muss die Shell nur bei jeder Schleifeniteration einen Prozess erzeugen undcat
kopiert die Ein- / Ausgabe. Das System kann den Dateiinhalt auch zwischenspeichern, um den Cat-Prozess zu beschleunigen.quelle
/*/*/*/*../../../../*/*/*/*/../../../../
im Dateiinhalt enthalten sein kann. Ich möchte nur auf die Details hinweisen .time echo $( <xdditg106) >/dev/null real 0m0.125s user 0m0.085s sys 0m0.025s
time echo "$( <xdditg106)" >/dev/null real 0m0.047s user 0m0.016s sys 0m0.022s
glob+split
Teil, und es wird die while-Schleife beschleunigen. Und ich habe auch bemerkt, dass es dir nicht viel hilft. Seit wann ist der Großteil des Shell-echo
Verhaltens nicht mehr POSIX-konform.printf '%s' "$i"
ist besser.Wenn Sie anrufen
Dadurch wächst Ihr Shell-Prozess um 50 MB auf bis zu 200 MB (abhängig von der internen Wide Character-Implementierung). Dies kann Ihre Shell verlangsamen, aber dies ist nicht das Hauptproblem.
Das Hauptproblem besteht darin, dass der obige Befehl die gesamte Datei in den Shell-Speicher einlesen und die
echo $i
Feldaufteilung für diesen Dateiinhalt in ausführen muss$i
. Um eine Feldaufteilung durchzuführen, muss der gesamte Text aus der Datei in breite Zeichen konvertiert werden, und hier wird die meiste Zeit verbracht.Ich habe einige Tests mit dem langsamen Fall durchgeführt und folgende Ergebnisse erhalten:
Der Grund, warum ksh93 am schnellsten ist, scheint darin zu liegen, dass ksh93 nicht
mbtowc()
von libc verwendet wird, sondern eine eigene Implementierung.Übrigens: Stephane ist der Meinung, dass die Lesegröße einen gewissen Einfluss hat. Ich habe die Bourne-Shell kompiliert, um 4096-Byte-Chunks anstelle von 128-Byte-Chunks einzulesen, und in beiden Fällen die gleiche Leistung erzielt.
quelle
i=`cat input`
Befehl führt keine Feldaufteilung durch, sondern nur dieecho $i
. Die dafür aufgewendete Zeiti=`cat input`
wird im Vergleich zuecho $i
, aber nicht im Vergleich zucat input
allein vernachlässigbar sein, und im Fall vonbash
, ist der Unterschied größtenteils aufbash
kleine Lesevorgänge zurückzuführen. Ein Wechsel von 128 auf 4096 hat keinen Einfluss auf die Leistung vonecho $i
, aber das war nicht der Punkt, den ich angesprochen habe.echo $i
je nach Inhalt der Eingabe und des Dateisystems (wenn es IFS- oder Glob-Zeichen enthält) erheblich variieren wird. Aus diesem Grund habe ich in meiner Antwort keinen Vergleich von Shells durchgeführt. Beispielsweise istyes | ghead -c50M
ksh93 bei der Ausgabe von die langsamste von allen, bei der Ausgabe von jedochyes | ghead -c50M | paste -sd: -
die schnellste.In beiden Fällen wird die Schleife nur zweimal ausgeführt (einmal für das Wort
seq
und einmal für das Wort10
).Außerdem werden beide benachbarte Leerzeichen zusammengeführt und führende / nachfolgende Leerzeichen entfernt, sodass die Ausgabe nicht unbedingt zwei Kopien der Eingabe enthält.
Zuerst
Zweite
Ein Grund, warum das
echo
langsamer ist, kann sein, dass Ihre Variable ohne Anführungszeichen in Leerzeichen in separate Wörter aufgeteilt wird. Für 50MB ist das eine Menge Arbeit. Zitiere die Variablen!Ich schlage vor, dass Sie diese Fehler beheben und dann Ihre Timings neu bewerten.
Ich habe das vor Ort getestet. Ich habe eine 50MB-Datei mit der Ausgabe von erstellt
tar cf - | dd bs=1M count=50
. Ich habe auch die Schleifen so erweitert, dass sie um den Faktor x100 laufen, sodass die Timings auf einen vernünftigen Wert skaliert wurden (ich habe eine weitere Schleife um Ihren gesamten Code hinzugefügt:for k in $(seq 100); do
...done
). Hier sind die Zeiten:Wie Sie sehen, gibt es keinen wirklichen Unterschied, aber wenn überhaupt,
echo
läuft die Version, die sie enthält, geringfügig schneller. Wenn ich die Anführungszeichen entferne und Ihre kaputte Version 2 ausführe, verdoppelt sich die Zeit und zeigt, dass die Shell weitaus mehr Arbeit leisten muss, als erwartet werden sollte.quelle
cat
ist sehr, sehr schneller alsecho
. Das erste Skript dauert durchschnittlich 3 Sekunden, das zweite hingegen durchschnittlich 54 Sekunden.tar cf - | dd bs=1M count=50
? Erstellt es eine reguläre Datei mit denselben Zeichen? In meinem Fall ist die Eingabedatei völlig unregelmäßig mit allen Arten von Zeichen und Leerzeichen. Und wieder habe ich verwendet,time
wie Sie verwendet haben, und das Ergebnis war das, was ich sagte: 54 Sekunden vs 3 Sekunden.read
ist viel schneller alscat
Ich denke, jeder kann das testen:
cat
dauert 9,372 Sekunden.echo
dauert.232
Sekunden.read
ist 40 mal schneller .Mein erster Test, als
$p
auf dem Bildschirm angezeigt wurde,read
war 48-mal schneller alscat
.quelle
Das
echo
soll 1 Zeile auf dem Bildschirm setzen. Was Sie im zweiten Beispiel tun, ist, dass Sie den Inhalt der Datei in eine Variable einfügen und dann diese Variable drucken. In der ersten setzen Sie den Inhalt sofort auf den Bildschirm.cat
ist für diese Nutzung optimiert.echo
ist nicht. Es ist keine gute Idee, 50 MB in eine Umgebungsvariable zu schreiben.quelle
echo
für das Schreiben von Text optimiert werden?Es geht nicht darum, dass Echo schneller ist, sondern darum, was Sie tun:
In einem Fall lesen Sie direkt von der Eingabe und schreiben zur Ausgabe. Mit anderen Worten, was von der Eingabe über cat gelesen wird, wird über stdout ausgegeben.
In dem anderen Fall lesen Sie von der Eingabe in eine Variable im Speicher und schreiben dann den Inhalt der Variablen in die Ausgabe.
Letzteres ist viel langsamer, insbesondere wenn der Eingang 50 MB beträgt.
quelle