Warum ist `tac file | grep foo '(Piping) schneller als `grep foo << (tac file)' (Prozessersetzung)?

7

Diese Frage wurde durch " Reverse Grepping " motiviert , bei dem es darum ging, eine riesige Datei von unten nach oben zu erfassen.

@chaos sagte :

tac file | grep whatever

Oder etwas effektiver:

grep whatever < <(tac file)

@ vinc17 sagte :

Das < <(tac filename)sollte so schnell wie ein Rohr sein

Es gibt auch viele interessante Kommentare von anderen Benutzern.

Meine Fragen:

  • Was ist der Unterschied zwischen |und < <()?
  • Warum ist einer schneller als der andere?
  • Und was ist wirklich schneller?
  • Warum hat niemand vorgeschlagen xargs?
c0rp
quelle
1
Beachten Sie, dass es < <(...)für 0< <(...)nicht steht <<(...).
Stéphane Chazelas
1
@Anthon Danke, dass du "greping" auf "grepping" korrigiert hast - ich bin gerade von english.SE zurückgekommen , um das Problem zu beheben. Ich wurde wirklich neugierig, wie es richtig ist und warum . In kurzer Zeit viel über Englisch gelernt. Siehe 6 Antworten unter Ableiten eines Wortes für die Aktivität der Verwendung eines Werkzeugs aus dem Werkzeugnamen ("grep")
Volker Siegel
@VolkerSiegel Ich habe mir gerade den Titel der verknüpften Frage angesehen, um eine einheitliche Schreibweise zu erhalten ;-)
Anthon
Ich wusste, dass es inkonsistent war - aber ich wusste nicht, wer Recht hat;) Ich habe "greping" stark bevorzugt, weil das zusätzliche "p" in "grepping" irgendwie Teil des Befehlsnamens wird - was sich für mich "sehr falsch" anfühlte - einfach machte überhaupt keinen Sinn ... Aber es stellte sich heraus, dass die Jungs bei English.SE mich dazu gebracht haben, "Grepping" in ungefähr einer Stunde und 6 Antworten als richtig zu akzeptieren :)
Volker Siegel
@ StéphaneChazelas Das Beispiel wurde entsprechend bearbeitet.
Volker Siegel

Antworten:

10

Die Konstruktion <(tac file)bewirkt, dass:

  • Erstellen Sie eine Pipe mit einem Namen
    • Auf Systemen wie Linux und SysV /dev/fdwird eine reguläre Pipe verwendet, /dev/fd/<the-file-descriptor-of-the-pipe>die als Name verwendet wird.
    • Auf anderen Systemen wird eine Named Pipe verwendet, für die ein tatsächlicher Dateieintrag auf der Festplatte erstellt werden muss.
  • Starten Sie den Befehl tac fileund verbinden Sie ihn mit einem Ende der Pipe.
  • Ersetzen Sie die gesamte Konstruktion in der Befehlszeile durch den Namen der Pipe.

Nach dem Ersetzen wird die Befehlszeile wie folgt:

grep whatever < /tmp/whatever-name-the-shell-used-for-the-named-pipe

Und dann grepwird es ausgeführt, und es liest seine Standardeingabe (die die Pipe ist), liest es und sucht darin nach seinem ersten Argument.

Das Endergebnis ist also das gleiche wie bei ...

tac file | grep whatever

... dass dieselben zwei Programme gestartet werden und immer noch eine Pipe verwendet wird, um sie zu verbinden. Die <( ... )Konstruktion ist jedoch komplizierter, da sie mehr Schritte umfasst und möglicherweise eine temporäre Datei (die Named Pipe) enthält.

Das <( ... )Konstrukt ist eine Erweiterung und weder in der Standard-POSIX-Bourne-Shell noch auf Plattformen verfügbar, die keine /dev/fdPipes unterstützen oder benennen. Allein aus diesem Grund ist die tragbarere command | other-commandForm die bessere Wahl , da die beiden in Betracht gezogenen Alternativen in ihrer Funktionalität genau gleichwertig sind .

Die <( ... )Konstruktion sollte wegen der zusätzlichen Faltung langsamer sein, aber es ist nur in der Startphase und ich erwarte nicht, dass der Unterschied leicht messbar ist.

ANMERKUNG : Verwendet auf Linux SysV-Plattformen < ( ... )keine Named Pipes, sondern reguläre Pipes. Auf reguläre Pipes (in der Tat alle Dateideskriptoren) kann mit dem speziellen Namen verwiesen werden, /dev/fd/<file-descriptor-numbersodass die Shell diesen Namen für die Pipe verwendet. Auf diese Weise wird vermieden, dass eine echte Named Pipe mit einem echten temporären Dateinamen im realen Dateisystem erstellt wird. Obwohl der /dev/fdTrick verwendet wurde, um diese Funktion zu implementieren, als sie ursprünglich angezeigt wurde ksh, handelt es sich um eine Optimierung: Auf Plattformen, die dies nicht unterstützen, wird wie oben beschrieben eine reguläre Named Pipe im realen Dateisystem verwendet.

AUCH HINWEIS : Um die Syntax als <<( ... )irreführend zu beschreiben . Tatsächlich ist es das <( ... ), was durch den Namen einer Pipe ersetzt wird, und dann ist das andere <Zeichen, das dem Ganzen vorangestellt ist, von dieser Syntax getrennt und es ist die reguläre bekannte Syntax zum Umleiten von Eingaben aus einer Datei.

Celada
quelle
1
Die Prozessersetzung wurde auf ksh angezeigt und verwendete von Anfang an / dev / fd / n (eine SysV-Funktion) (und war auf Systemen, die dies nicht unterstützten, nicht verfügbar). bashund zshUnterstützung für Named Pipes für Systeme hinzugefügt, denen später / dev / fd / n fehlte.
Stéphane Chazelas
@ StéphaneChazelas ah, das ist interessante Geschichte, danke. Es ist also ursprünglich eine kshFunktion, keine Standard-POSIX-Funktion! Sicher genug, dashscheint es nicht zu unterstützen. Dies ist ein weiterer Grund, dies zu vermeiden, wenn eine gleichwertige Alternative mit grundlegenden POSIX-Funktionen verfügbar ist.
Celada
Es ist nicht nur SysV (SysV bezieht sich im Allgemeinen auf eine Referenzimplementierung und auch auf eine Familie von Betriebssystemen) und Linux. Solaris, alle modernen BSDs und wahrscheinlich viele andere unterstützen / dev / fd / n. (Tatsächlich unterscheidet sich Linux erheblich von anderen, da es als Symlinks zu Originalressourcen implementiert ist, während in anderen Unices / dev / fd / n spezielle Geräte sind, die im geöffneten Zustand wie dup funktionieren (weshalb ich unter Linux sagte: und nur Linux, / dev / fd / n auf einer Pipe fungiert als Named Pipe)).
Stéphane Chazelas
1
Ich denke, dass eine Prozessersetzung < <(command)in Bezug auf das Rohr vorzuziehen ist |. Wie von gnouc erläutert, |muss jeder Befehl in einer Subshell ausgeführt werden, während die Prozessersetzung die Pipe offen lässt und den Befehl von der Standardausgabe speist. Es gibt mehr hier: wiki.bash-hackers.org/syntax/expansion/proc_subst und mywiki.wooledge.org/ProcessSubstitution
Valentin Bajrami
@ val0x00ff Die command | commandRohrkonstruktion ist so effizient wie möglich. Die Shell fork()erstellt jeden neuen Prozess in einer Pipeline. wird dann fork()aber universell verwendet, um neue Prozesse zu erstellen - das wird nicht als Subshell betrachtet.
Celada
7

Was ist der Unterschied zwischen | und << ()?

Es gibt einen Unterschied zwischen ihnen:

  • | Jeder Befehl wird in einer separaten Unterschale ausgeführt.

  • <() Führen Sie den Befehl aus, der im Hintergrund ersetzt wird.

Für die nächsten beiden Fragen werden wir einige tun strace:

pipe::

$ strace -fc bash -c 'tac /usr/share/dict/american-english | grep qwerty'
$ time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.008120        2707         3         1 wait4
  0.00    0.000000           0       352           read
  0.00    0.000000           0       229           write
  0.00    0.000000           0        20         2 open
  0.00    0.000000           0        29         2 close
  0.00    0.000000           0        40        17 stat
  0.00    0.000000           0        19           fstat
  0.00    0.000000           0       117           lseek
  0.00    0.000000           0        38           mmap
  0.00    0.000000           0        18           mprotect
  0.00    0.000000           0         6           munmap
  0.00    0.000000           0        25           brk
  0.00    0.000000           0        22           rt_sigaction
  0.00    0.000000           0        18           rt_sigprocmask
  0.00    0.000000           0         1           rt_sigreturn
  0.00    0.000000           0         3         2 ioctl
  0.00    0.000000           0        24        12 access
  0.00    0.000000           0         1           pipe
  0.00    0.000000           0         2           dup2
  0.00    0.000000           0         1           getpid
  0.00    0.000000           0         1         1 getpeername
  0.00    0.000000           0         2           clone
  0.00    0.000000           0         3           execve
  0.00    0.000000           0         1           uname
  0.00    0.000000           0         1           getrlimit
  0.00    0.000000           0        13           getuid
  0.00    0.000000           0        13           getgid
  0.00    0.000000           0        13           geteuid
  0.00    0.000000           0        13           getegid
  0.00    0.000000           0         1           getppid
  0.00    0.000000           0         1           getpgrp
  0.00    0.000000           0         3           arch_prctl
  0.00    0.000000           0         1           time
------ ----------- ----------- --------- --------- ----------------
100.00    0.008120                  1034        37 total

Process Substitution::

$ strace -fc bash -c 'grep qwerty < <(tac /usr/share/dict/american-english)'
$ time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 99.14    0.016001        4000         4         2 wait4
  0.46    0.000075           0       229           write
  0.24    0.000038           0       341           read
  0.16    0.000026           1        24           brk
  0.00    0.000000           0        21         2 open
  0.00    0.000000           0        27           close
  0.00    0.000000           0        40        17 stat
  0.00    0.000000           0        19           fstat
  0.00    0.000000           0       117           lseek
  0.00    0.000000           0        38           mmap
  0.00    0.000000           0        18           mprotect
  0.00    0.000000           0         6           munmap
  0.00    0.000000           0        35           rt_sigaction
  0.00    0.000000           0        24           rt_sigprocmask
  0.00    0.000000           0         2           rt_sigreturn
  0.00    0.000000           0         3         2 ioctl
  0.00    0.000000           0        24        12 access
  0.00    0.000000           0         1           pipe
  0.00    0.000000           0         3           dup2
  0.00    0.000000           0         1           getpid
  0.00    0.000000           0         1         1 getpeername
  0.00    0.000000           0         3           clone
  0.00    0.000000           0         3           execve
  0.00    0.000000           0         1           uname
  0.00    0.000000           0         1         1 fcntl
  0.00    0.000000           0         2           getrlimit
  0.00    0.000000           0        13           getuid
  0.00    0.000000           0        13           getgid
  0.00    0.000000           0        13           geteuid
  0.00    0.000000           0        13           getegid
  0.00    0.000000           0         1           getppid
  0.00    0.000000           0         1           getpgrp
  0.00    0.000000           0         3           arch_prctl
  0.00    0.000000           0         1           time
------ ----------- ----------- --------- --------- ----------------
100.00    0.016140                  1046        37 total

Warum etwas schneller als andere?

Und was ist wirklich schneller?

Sie sehen, process substitutionist langsamer als pipein diesem Fall, weil es mehr Systemaufruf verwendet. Beide verbringen viel Zeit damit, auf untergeordnete Prozesse zu warten, verwenden jedoch process substitutionmehr wait4()Systemaufruf und mehr Zeit für jeden Anruf als pipe.

Warum schlägt niemand Xargs vor?

Ich denke, hier xargskann nichts helfen, das ist nicht seine Aufgabe.

Aktualisieren

Wie von @ Gilles vorgeschlagen, mache ich einen Test mit einer größeren Datei, aus der 2 GB zufällige Daten generiert wurden /dev/urandom. Es zeigt, dass pipedas wirklich schneller ist als process substitution.

pipe::

$ strace -fc bash -c 'tac sample.txt | grep qwerty'
$ time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 81.15    8.284959     2761653         3         1 wait4
 17.89    1.825959           2    780959           read
  0.91    0.092708           0    524286           write
  0.05    0.005364           0    262146           lseek
  0.00    0.000000           0        20         2 open
  0.00    0.000000           0        29         2 close
  0.00    0.000000           0        40        17 stat
  0.00    0.000000           0        19           fstat
  0.00    0.000000           0        38           mmap
  0.00    0.000000           0        18           mprotect
  0.00    0.000000           0         6           munmap
  0.00    0.000000           0        25           brk
  0.00    0.000000           0        22           rt_sigaction
  0.00    0.000000           0        18           rt_sigprocmask
  0.00    0.000000           0         1           rt_sigreturn
  0.00    0.000000           0         3         2 ioctl
  0.00    0.000000           0        24        12 access
  0.00    0.000000           0         1           pipe
  0.00    0.000000           0         2           dup2
  0.00    0.000000           0         1           getpid
  0.00    0.000000           0         1         1 getpeername
  0.00    0.000000           0         2           clone
  0.00    0.000000           0         3           execve
  0.00    0.000000           0         1           uname
  0.00    0.000000           0         1           getrlimit
  0.00    0.000000           0        13           getuid
  0.00    0.000000           0        13           getgid
  0.00    0.000000           0        13           geteuid
  0.00    0.000000           0        13           getegid
  0.00    0.000000           0         1           getppid
  0.00    0.000000           0         1           getpgrp
  0.00    0.000000           0         3           arch_prctl
  0.00    0.000000           0         1           time
------ ----------- ----------- --------- --------- ----------------
100.00   10.208990               1567727        37 total

process substitution::

$ strace -fc bash -c 'grep qwerty < <(tac sample.txt)'
$ time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 99.51   13.912869     3478217         4         2 wait4
  0.38    0.053373           0    655269           read
  0.09    0.013084           0    524286           write
  0.02    0.002454           0    262146           lseek
  0.00    0.000030           1        38           mmap
  0.00    0.000024           1        24        12 access
  0.00    0.000000           0        21         2 open
  0.00    0.000000           0        27           close
  0.00    0.000000           0        40        17 stat
  0.00    0.000000           0        19           fstat
  0.00    0.000000           0        18           mprotect
  0.00    0.000000           0         6           munmap
  0.00    0.000000           0        24           brk
  0.00    0.000000           0        35           rt_sigaction
  0.00    0.000000           0        24           rt_sigprocmask
  0.00    0.000000           0         2           rt_sigreturn
  0.00    0.000000           0         3         2 ioctl
  0.00    0.000000           0         1           pipe
  0.00    0.000000           0         3           dup2
  0.00    0.000000           0         1           getpid
  0.00    0.000000           0         1         1 getpeername
  0.00    0.000000           0         3           clone
  0.00    0.000000           0         3           execve
  0.00    0.000000           0         1           uname
  0.00    0.000000           0         1         1 fcntl
  0.00    0.000000           0         2           getrlimit
  0.00    0.000000           0        13           getuid
  0.00    0.000000           0        13           getgid
  0.00    0.000000           0        13           geteuid
  0.00    0.000000           0        13           getegid
  0.00    0.000000           0         1           getppid
  0.00    0.000000           0         1           getpgrp
  0.00    0.000000           0         3           arch_prctl
  0.00    0.000000           0         1           time
------ ----------- ----------- --------- --------- ----------------
100.00   13.981834               1442060        37 total
cuonglm
quelle
1
Bei der Substitution von Prozessen werden /dev/fd/nkeine Named Pipes verwendet (obwohl unter Linux und nur Linux / dev / fd / n sich wie Named Pipes verhalten, wenn n ein Dateideskriptor für eine Pipe ist (benannt oder nicht)). Auf Systemen, die / dev / fd / n nicht unterstützen, verwenden einige Shells Named Pipes.
Stéphane Chazelas
Notiz hinzugefügt. Es scheint jedoch, dass die Prozesssubstitution nur unterstützt wird, wenn das System Named Pipe unterstützt: gnu.org/software/bash/manual/html_node/… . Und daraus: en.wikipedia.org/wiki/Process_substitution , hieß es, Prozesssubstitution benenne Pipe?
Cuonglm
Nun, Wikipedia ist hier falsch (und bash-zentriert). Führen Sie einfach aus, ls -l <(:)um sicherzustellen, dass keine Named Pipes verwendet werden.
Stéphane Chazelas
1
@Gnouc Eine Named Pipe ist eine Pipe, die mit einem Namen im Dateisystem verbunden ist. Es wird mkfifowie im Wikipedia-Artikel beschrieben erstellt. Eine Named Pipe ist einer dieser "speziellen" Dateitypen, die manchmal im Dateisystem angezeigt werden, z. B. UNIX-Domänensockets, Zeichengeräte und Blockgeräte.
Celada
1
Wenn ich den Befehl strace mehrmals ausführe, stelle ich fest, dass sich die Ausgabe etwas ändert und dass beide Lösungen ziemlich gleichwertig sind. Dies wird in einer großen Datei (mehrere MB) noch sichtbarer.
vinc17
1

Ich konnte die von cuonglm gezeigten Ergebnisse nicht replizieren . Selbst mit einer 2-GB-Datei sehe ich in Bash 5 unter MacOS Mojave sehr ähnliche Zeitpunkte zwischen Prozessersetzung und Pipe. Dies ist für mich sinnvoll, da der mit einem Aufruf verbundene Overhead im Vergleich zur tatsächlichen Verarbeitung dieses Aufrufs für eine 2-GB-Datei minimal sein wird. Das Ausführen einer Iteration der Verwendung der Prozessersetzung gegenüber einer Pipe hängt also von der Zufälligkeit ab / welcher Befehl war Führen Sie zuerst aus, um den Dateiinhalt zwischenzuspeichern.

Ich war der Lage , die Ergebnisse zu replizieren in dieser Frage , die zeigen , dass Prozess Substitution schneller als Rohre über mehrere tausend Instanzen dieser Anrufe.

Hier sind die Befehle, die ich ausgeführt habe, und die Ausgabe:

pipe.sh :

shopt -s lastpipe
for i in {1..5000}; do
    echo foo bar |
    while read; do
        echo $REPLY >/dev/null
    done
done

proc-sh :

for i in {1..5000}; do
    while read; do
        echo $REPLY >/dev/null
    done < <(echo foo bar)
done

pipe-no-lastpipe.sh :

for i in {1..5000}; do
    echo foo bar |
    while read; do
        echo $REPLY >/dev/null
    done
done

Test :

time ./proc-sub.sh

real    0m9.505s
user    0m1.875s
sys     0m10.705s

time ./pipe.sh

real    0m14.036s
user    0m4.583s
sys     0m14.193s

time ./pipe-no-lastpipe.sh

real    0m16.696s
user    0m3.055s
sys     0m18.057s
jeremysprofile
quelle