Optimieren Dateideskriptoren das Schreiben in Dateien?

7

Ist es gleichbedeutend damit, dass Befehle direkt in eine Datei gedruckt werden, anstatt in einen Dateideskriptor zu schreiben?

Illustration

Direkt in die Datei schreiben:

for i in {1..1000}; do >>x echo "$i"; done

Verwenden eines fd:

exec 3>&1 1>x
for i in {1..1000}; do echo "$i"; done
exec 1>&3 3>&-

Ist letzteres effizienter?

Gilles 'SO - hör auf böse zu sein'
quelle
5
beides write(2)zu einem Dateideskriptor, wie denkst du ist einer "direkter"?
Thrig
3
Was ist mit for i in {1..1000}; do echo "$i"; done > x? Das öffnet die Datei nur einmal.
RonJohn
@ RonJohn Es ist bereits in Matija Nalis 'Antwort erwähnt.

Antworten:

12

Der Hauptunterschied zwischen dem Öffnen der Datei vor der Schleife mit execund dem Einfügen der Umleitung in den Befehl in die Schleife besteht darin, dass beim ersteren der Dateideskriptor nur einmal eingerichtet werden muss, während beim letzteren die Datei für jede Iteration der Schleife geöffnet und geschlossen wird.

Dies einmal zu tun ist wahrscheinlich effizienter, aber wenn Sie einen externen Befehl innerhalb der Schleife ausführen würden, würde der Unterschied wahrscheinlich in den Kosten für das Starten des Befehls verschwinden. ( echohier ist wahrscheinlich eingebaut, das gilt also nicht)

Wenn die Ausgabe an eine andere als eine reguläre Datei gesendet werden soll (z. B. wenn xes sich um eine Named Pipe handelt), kann das Öffnen und Schließen der Datei für andere Prozesse sichtbar sein, sodass es auch zu Verhaltensunterschieden kommen kann.


Beachten Sie, dass es wirklich keinen Unterschied zwischen einer Umleitung durch execund einer Umleitung im Befehl gibt. Beide öffnen die Datei und jonglieren mit den Dateideskriptornummern.

Diese beiden sollten ziemlich gleichwertig sein, da sie sowohl open()die Datei als auch write()dazu sind. (Es gibt jedoch Unterschiede in der Speicherung von fd 1 für die Dauer des Befehls.):

for i in {1..1000}; do 
    >>x echo "$i"
done


for i in {1..1000}; do
    exec 3>&1 1>>x         # assuming fd 3 is available
    echo "$i"              # here, fd 3 is visible to the command
    exec 1>&3 3>&-
done

ilkkachu
quelle
6
Natürlich können Sie auch die gesamte Schleife in x umleiten : for i in {1..1000}; do echo $i; done >x. Ich würde sagen, dies ist die am besten lesbare Version.
Schlagen Sie Bolli
Ich habe den Unterschied zwischen beiden in jeder Iteration für 1 m Iterationen gemessen, und der Unterschied ist ziemlich groß (70-90%). Siehe meine Antwort: unix.stackexchange.com/a/444729/181255 Das Verhältnis ist mit nur 1k Iterationen (ca. 300%) sogar noch höher.
7

Ja, es ist effizienter

Der einfachste Weg zum Testen besteht darin, die Anzahl auf 500000 zu erhöhen und die Zeit festzulegen:

> time bash s1.sh; time bash s2.sh
bash s1.sh  16,47s user 10,00s system 99% cpu 26,537 total
bash s2.sh  10,51s user 3,50s system 99% cpu 14,008 total

strace (1) zeigt warum (wir haben eine einfache writeanstelle von open+ 5 * fcntl+ 2 * dup+ 2 * close+ write):

denn for i in {1..1000}; do >>x echo "$i"; donewir bekommen:

open("x", O_WRONLY|O_CREAT|O_APPEND, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 10
fcntl(1, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0
write(1, "997\n", 4)                    = 4
dup2(10, 1)                             = 1
fcntl(10, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
close(10)                               = 0
open("x", O_WRONLY|O_CREAT|O_APPEND, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 10
fcntl(1, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0
write(1, "998\n", 4)                    = 4
dup2(10, 1)                             = 1
fcntl(10, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
close(10)                               = 0
open("x", O_WRONLY|O_CREAT|O_APPEND, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 10
fcntl(1, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0
write(1, "999\n", 4)                    = 4
dup2(10, 1)                             = 1
fcntl(10, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
close(10)                               = 0
open("x", O_WRONLY|O_CREAT|O_APPEND, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 10
fcntl(1, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0
write(1, "1000\n", 5)                   = 5
dup2(10, 1)                             = 1
fcntl(10, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
close(10)                               = 0

während exec 3>&1 1>xwir viel sauberer werden

write(1, "995\n", 4)                    = 4
write(1, "996\n", 4)                    = 4
write(1, "997\n", 4)                    = 4
write(1, "998\n", 4)                    = 4
write(1, "999\n", 4)                    = 4
write(1, "1000\n", 5)                   = 5

Beachten Sie jedoch, dass der Unterschied nicht auf die Verwendung eines FD zurückzuführen ist, sondern auf den Ort, an dem Sie die Umleitung durchführen. Wenn Sie dies beispielsweise tun for i in {1..1000}; do echo "$i"; done > x würden, würden Sie fast die gleiche Leistung wie in Ihrem zweiten Beispiel erzielen:

bash s3.sh  10,35s user 3,70s system 100% cpu 14,042 total
Matija Nalis
quelle
0

Um die Dinge zusammenzufassen und neue Informationen in diesen Thread aufzunehmen, finden Sie hier einen Vergleich von vier Möglichkeiten, geordnet nach Effizienz. Ich schätze die Effizienz durch Zeitmessung (Benutzer + System) für 1 Million Iterationen basierend auf zwei Testreihen.

  1. Diese beiden sind ungefähr gleich:
    • Einfache >Schleifenumleitung (Zeit: 100% )
    • Mit execeinmal für die gesamte Schleife (Zeit: ~ 100% )
  2. Verwendung >>für jede Iteration (Zeit: 200% - 250% )
  3. Verwendung execfür jede Iteration (Zeit: 340% - 480% )

Die Schlussfolgerung lautet:

Es gibt einen kleinen Unterschied zwischen der Verwendung execund einfachen Umleitungen wie >>. (Einfach ist billiger). Es wird nicht auf der Ebene der Ausführung eines einzelnen Befehls angezeigt, aber bei einer hohen Anzahl von Wiederholungen wird der Unterschied sichtbar. Obwohl das Ausführungsgewicht des Befehls auf die Unterschiede umgeleitet wurde, wie von ikkachu in der anderen Antwort bemerkt.


quelle
Die relativen Kosten hängen vom Betriebssystem und der Hardware sowie von den Kosten für das, was sich in der Schleife befindet, ab. (z. B. bei x86 hat die Reduzierung von Spectre + Meltdown die Kosten für Systemaufrufe erheblich erhöht und die Umleitung innerhalb der Schleife verschlechtert.) Wenn Sie buchstäblich eine Folge von Ganzzahlen generieren möchten seq 1000oder möglicherweise printf "%s\n" {1..1000}schneller als eine Bash-Schleife sind. ( seq 1000000 > /dev/nullLäuft time printf "%s\n" {1..1000000} > /dev/nullin ~ 0,03 s, in ~ 0,7 s, time for i in {1..1000000}; do echo "$i"; done > /dev/nullin 2,1 s auf einem 3,9 GHz Skylake i7-6700k, Linux 4.15.8-1-ARCH.
Peter Cordes
Jetzt bin ich gespannt, wo der Break-Even-Punkt für Builtin printfvs. Fork + Exec von liegt seq. Ich denke, ich könnte eine Wiederholungsschleife um das Ganze wickeln und das mal. Übrigens time awk 'BEGIN{ for(i=1; i<1000000 ; i++) {print i}}' > /dev/nullist es ungefähr dreimal schneller als printf "%s\n" {1..1000000}, selbst wenn Sie es nicht haben seq, kann eine gute AWK-Implementierung integrierte Funktionen für große Problemgrößen übertreffen.
Peter Cordes