Warum gibt es einen solchen Unterschied in der Ausführungszeit von Echo und Katze?


15

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 echound 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 Manpage echodavon 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 wie i. Dies ist jedoch nur eine Vermutung.)
  • Und warum habe ich Probleme, wenn ich benutze echo?

AKTUALISIEREN

Ich habe seq 10statt `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: catArbeitet viel schneller als echo.


1
und was ist cat $(for i in $(seq 1 10); do echo "input"; done) >> output? :)
netmonk

2
Das echogeht 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.
Roaima

Das Zitieren der Variablen ist nicht das Problem; Das Problem ist die Variable i selbst (dh sie wird als Zwischenschritt zwischen Eingabe und Ausgabe verwendet).
Aleksander

`echo $ i` - mach das nicht. Verwenden Sie printf und zitieren Sie das Argument.
PSkocik

1
@PSkocik Was ich sage ist, dass du willst printf '%s' "$i", nicht echo $i. @cuonglm erklärt einige der Echoprobleme gut in seiner Antwort. Warum in einigen Fällen mit Echo nicht einmal das Zitieren ausreicht, erfahren
PSkocik,

Antworten:


24

Hier sind einige Dinge zu beachten.

i=`cat input`

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 iVariablen 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.

zshNachbearbeitung erforderlich, um NUL-Bytes zu umgehen (andere Shells setzen NUL-Bytes außer Kraft), und yashnoch 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 echoist 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 yashFall ist) Suchen Sie nach $IFSZeichen in der Zeichenfolge. Wenn $IFSLeerzeichen, 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 ( bashs 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 echowird in der Regel eine zusätzliche Verarbeitung durchgeführt. Einige Implementierungen erweitern \xSequenzen 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 catnicht 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. catliest 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 printfanstelle von echo) verwenden.

Wenn Sie es weiter optimieren möchten, anstatt es catmehrmals aufzurufen , übergeben Sie es einfach inputmehrmals an cat.

yes input | head -n 100 | xargs cat

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

zmodload zsh/mapfile
var=$mapfile[input]
repeat 10 print -rn -- "$var"

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 printfkeine eingebauten Daten vorliegen) mit:

i=$(cat input && echo .) || exit # add an extra .\n to avoid trimming newlines
i=${i%.} # remove that trailing dot (the \n was removed by cmdsubst)
n=10
while [ "$n" -gt 10 ]; do
  printf %s "$i"
  n=$((n - 1))
done

Aber das wird niemals effizienter als die Verwendung catin der Schleife (es sei denn, die Eingabe ist sehr klein).


Es ist erwähnenswert, dass bei langen Auseinandersetzungen das Gedächtnis verloren gehen kann . Beispiel/bin/echo $(perl -e 'print "A"x999999')
cuonglm

Sie irren sich mit der Annahme, dass die Lesegröße einen signifikanten Einfluss hat. Lesen Sie meine Antwort, um den wahren Grund zu verstehen.
Schily

@schily, 409600 Lesevorgänge von 128 Bytes erfordern mehr Zeit (Systemzeit) als 800 Lesevorgänge von 64.000. Vergleichen Sie dd bs=128 < input > /dev/nullmit dd bs=64 < input > /dev/null. Von den 0,6s, die zum Lesen dieser Datei readerforderlich sind, werden 0,4 in diesen Systemaufrufen in meinen Tests ausgegeben , während andere Shells viel weniger Zeit dort verbringen.
Stéphane Chazelas

Nun, Sie scheinen keine echte Leistungsanalyse durchgeführt zu haben. Der Einfluss des Leseaufrufs (beim Vergleich verschiedener Lesegrößen) beträgt ungefähr 4%. 1% der gesamten Zeit, während die Funktionen readwc() und trim()in der Burne Shell 30% der gesamten Zeit in Anspruch nehmen, und dies wird höchstwahrscheinlich unterschätzt, da es keine libc mit gprofAnnotation für gibt mbtowc().
Schily

Um welche wird \xerweitert?
Mohammad

11

Das Problem ist nicht etwa catund echo, es geht um das Zitat Variable vergessen $i.

zshWenn Sie in einem Bourne-ähnlichen Shell-Skript (außer ) Variablen nicht in Anführungszeichen setzen, werden die Variablen von glob+splitOperatoren bearbeitet .

$var

ist eigentlich:

glob(split($var))

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+splitdies zu verhindern, aber es wird Ihnen nicht viel helfen, da die Shell immer noch das große Zeichenfolgenargument erstellen und dessen Inhalt nach echo(Ersetzen von builtin echodurch external /bin/echoführt dazu, dass die Argumentliste zu lang ist oder nicht genügend Speicherplatz zur Verfügung steht abhängig von der $iGröße). Die meisten echoImplementierungen sind nicht POSIX-konform. Sie erweitern die Backslash- \xSequenzen in den empfangenen Argumenten.

Mit catmuss die Shell nur bei jeder Schleifeniteration einen Prozess erzeugen und catkopiert die Ein- / Ausgabe. Das System kann den Dateiinhalt auch zwischenspeichern, um den Cat-Prozess zu beschleunigen.


2
@roaima: Sie haben den Glob-Teil nicht erwähnt, was ein großer Grund sein kann, etwas abzubilden, das /*/*/*/*../../../../*/*/*/*/../../../../im Dateiinhalt enthalten sein kann. Ich möchte nur auf die Details hinweisen .
Dienstag,

Ich danke Ihnen. Selbst ohne das verdoppelt sich das Timing, wenn eine Variable ohne
Anführungszeichen verwendet wird

1
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
Netmonk

Ich habe nicht verstanden, warum das Zitieren das Problem nicht lösen kann. Ich brauche mehr Beschreibung.
Mohammad

1
@ mohammad.k: Wie ich in meiner Antwort geschrieben habe, verhindern Zitatvariable glob+splitTeil, 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- echoVerhaltens nicht mehr POSIX-konform. printf '%s' "$i"ist besser.
Dienstag,

2

Wenn Sie anrufen

i=`cat input`

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 $iFeldaufteilung 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:

  • Am schnellsten ist ksh93
  • Als nächstes kommt meine Bourne Shell (2x langsamer als ksh93)
  • Weiter ist bash (3x langsamer als ksh93)
  • Letzteres ist ksh88 (7x langsamer als ksh93)

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.


Der i=`cat input`Befehl führt keine Feldaufteilung durch, sondern nur die echo $i. Die dafür aufgewendete Zeit i=`cat input`wird im Vergleich zu echo $i, aber nicht im Vergleich zu cat inputallein vernachlässigbar sein, und im Fall von bash, ist der Unterschied größtenteils auf bashkleine Lesevorgänge zurückzuführen. Ein Wechsel von 128 auf 4096 hat keinen Einfluss auf die Leistung von echo $i, aber das war nicht der Punkt, den ich angesprochen habe.
Stéphane Chazelas

Beachten Sie auch, dass die Leistung von echo $ije 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 ist yes | ghead -c50Mksh93 bei der Ausgabe von die langsamste von allen, bei der Ausgabe von jedoch yes | ghead -c50M | paste -sd: -die schnellste.
Stéphane Chazelas

Als ich über die Gesamtzeit sprach, sprach ich über die gesamte Implementierung und ja, natürlich geschieht die Feldaufteilung mit dem Echo-Befehl. und hier wird die meiste Zeit verbracht.
Schily

Sie haben natürlich Recht, dass die Leistung vom Inhalt von $ i abhängt.
Schily

1

In beiden Fällen wird die Schleife nur zweimal ausgeführt (einmal für das Wort seqund einmal für das Wort 10).

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

#!/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

Ein Grund, warum das echolangsamer 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:

time ./1.sh

real    0m5.948s
user    0m0.012s
sys     0m0.064s

time ./2.sh

real    0m5.639s
user    0m4.060s
sys     0m0.224s

Wie Sie sehen, gibt es keinen wirklichen Unterschied, aber wenn überhaupt, echolä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.

time ./2original.sh

real    0m12.498s
user    0m8.645s
sys     0m2.732s

Tatsächlich läuft die Schleife 10 Mal, nicht zweimal.
fpmurphy

Ich habe getan, wie Sie gesagt haben, aber das Problem wurde nicht gelöst. catist sehr, sehr schneller als echo. Das erste Skript dauert durchschnittlich 3 Sekunden, das zweite hingegen durchschnittlich 54 Sekunden.
Mohammad

@ fpmurphy1: Nein. Ich habe meinen Code ausprobiert. Die Schleife läuft nur zweimal, nicht zehnmal.
Mohammad

@ mohammad.k zum dritten Mal: ​​Wenn Sie Ihre Variablen zitieren, verschwindet das Problem.
Roaima

@roaima: Was macht der Befehl 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, timewie Sie verwendet haben, und das Ergebnis war das, was ich sagte: 54 Sekunden vs 3 Sekunden.
Mohammad

-1

read ist viel schneller als cat

Ich denke, jeder kann das testen:

$ cd /sys/devices/system/cpu/cpu0/cpufreq
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do read p < scaling_cur_freq ; done

real    0m0.232s
user    0m0.139s
sys     0m0.088s
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do cat scaling_cur_freq > /dev/null ; done

real    0m9.372s
user    0m7.518s
sys     0m2.435s
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a read
read is a shell builtin
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a cat
cat is /bin/cat

catdauert 9,372 Sekunden. echodauert .232Sekunden.

readist 40 mal schneller .

Mein erster Test, als $pauf dem Bildschirm angezeigt wurde, readwar 48-mal schneller als cat.


-2

Das echosoll 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.

catist für diese Nutzung optimiert. echoist nicht. Es ist keine gute Idee, 50 MB in eine Umgebungsvariable zu schreiben.


Neugierig. Warum sollte nicht echofür das Schreiben von Text optimiert werden?
Roaima

2
Der POSIX-Standard enthält nichts, was besagt, dass Echo eine Zeile auf einem Bildschirm darstellen soll.
fpmurphy

-2

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.

input -> output

In dem anderen Fall lesen Sie von der Eingabe in eine Variable im Speicher und schreiben dann den Inhalt der Variablen in die Ausgabe.

input -> variable
variable -> output

Letzteres ist viel langsamer, insbesondere wenn der Eingang 50 MB beträgt.


Ich denke, Sie müssen erwähnen, dass cat die Datei zusätzlich zum Kopieren von stdin und zum Schreiben von stdout öffnen muss. Dies ist die Exzellenz des zweiten Skripts, aber das erste ist insgesamt sehr viel besser als das zweite.
Mohammad

Das zweite Drehbuch enthält keine herausragenden Leistungen. In beiden Fällen muss cat die Eingabedatei öffnen. Im ersten Fall geht der Standardwert von cat direkt in die Datei. Im zweiten Fall wird die Standardausgabe von cat zuerst an eine Variable übergeben, und anschließend wird die Variable in die Ausgabedatei gedruckt.
Aleksander

@ mohammad.k, im zweiten Drehbuch gibt es nachdrücklich keine "Exzellenz".
Wildcard
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.