staging.inyokaproject.org

Pipelining

Status: Gelöst | Ubuntu-Version: Xubuntu 22.04 (Jammy Jellyfish)
Antworten |

user_unknown

Avatar von user_unknown

Anmeldungsdatum:
10. August 2005

Beiträge: 17432

Ich habe 3 Programme in einer Pipeline.

Das erste erzeugt Daten in gemütlicher Form, so dass es übersichtlich bleibt, genauer gesagt eine Zahl pro Zeile, von 0 bis 33 mit einer kl. Pause:

1
2
3
4
5
6
#!/bin/bash
for z in {0..33}
do
	echo $z
	sleep 0.1
done 

Das zweite ist ein Filter, der nur Zahlen mit einer geraden Ziffer durchlässt. In Serie pröppeln die Daten aus der Pipeline raus:

1
./p1.sh | egrep "[2468]" 

Das dritte ist ein zweiter Filter, nämlich head, und das nimmt nur 10 Zeilen aus der Pipeline entgegen. Hänge ich das hinten dran, dann tut sich am Bildschirm erst mal nichts, aber dann erscheint der Output in einem Rutsch:

1
./p1.sh | egrep "[2468]" | head 

Nur die Datenquelle und head, und die Daten pröppeln wieder:

1
./p1.sh | head 

Erst head und dann egrep, und sie kommen auch auf einmal mit einem Rutsch nach Funkstille zu Beginn.

1
./p1.sh | head | egrep "[2468]"

Kann jemand erklären, wieso das so ist? Wieso kommen bei 2 Filtern die Daten nicht auch tröpfchenweise?

ChickenLipsRfun2eat Team-Icon

Supporter
Avatar von ChickenLipsRfun2eat

Anmeldungsdatum:
6. Dezember 2009

Beiträge: 12070

Ich vermute mal, dass das am buffering liegt. Versuch mal

stdbuf -i0 -o0 -e0 PROGRAMM

Das sollte™ das buffering ausschalten.

/ Nachtrag: Das mit „in einem Rutsch“ ist auch logisch, da head ja auf die 10 Zeilen wartet. Das sleep ist da schon vorbei, das gilt ja nur während der Erzeugung. Bspw:

#!/bin/bash
function erzeugen {
  for z in {0..33}; do echo $z; sleep 0.1; done
}

erzeugen | grep -E '[2468]'
echo "----"
erzeugen |head -n10 | grep -E '[2468]'
echo "----"
erzeugen | grep -E '[2468]' | head -n10
echo "----"

ChickenLipsRfun2eat Team-Icon

Supporter
Avatar von ChickenLipsRfun2eat

Anmeldungsdatum:
6. Dezember 2009

Beiträge: 12070

Ich versuche es nochmal im Klartext, weil ich mir nicht sicher bin, ob die Aussage klar wird 😉

Wenn du x | head -n10 machst, wird die komplette Ausgabe von x nach 10 Zeilen abgeschnitten. Wenn du x | grep | head machst, wirft grep erst nach Beenden das EOF, was nach abarbeiten der while-Schleife kommt.

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 12527

user_unknown schrieb:

Das dritte ist ein zweiter Filter, nämlich head, und das nimmt nur 10 Zeilen aus der Pipeline entgegen. Hänge ich das hinten dran, dann tut sich am Bildschirm erst mal nichts, aber dann erscheint der Output in einem Rutsch:

1
./p1.sh | egrep "[2468]" | head 

Erst head und dann egrep, und sie kommen auch auf einmal mit einem Rutsch nach Funkstille zu Beginn.

1
./p1.sh | head | egrep "[2468]"

Kann jemand erklären, wieso das so ist? Wieso kommen bei 2 Filtern die Daten nicht auch tröpfchenweise?

Wenn Du in der Variante mit zwei Filtern und egrep als erstem Filter die Option "--line-buffered" für egrep verwendest, dann tröpfeln die Ausgaben ebenfalls zeilenweise. head hat so eine Option nicht, aber man kann als Ersatz sed -nue '1,10p' oder sed '10q' nutzen. Wenn man diesen Filter an der ersten Stelle hat und egrep als zweiten, sieht man ebenfalls den Tröpfeleffekt.

Meine Vermutung ist, dass egrep und head bei der Ausgabe auf das Terminal nur zeilenweise Puffern, während sie bei der Ausgabe in eine Pipe einen größeren Puffer verwenden. Grund könnte sein, dass man innerhalb der Pipeline davon ausgeht, dass tendenziell eher größere Datenmengen bewegt werden, womit ein größerer Puffer die Sache deutlich effektiver macht. Bei der Ausgabe in ein Terminal hingegen geht man vermutlich eher davon aus, dass da ein Mensch lesen will, und bevorzugt schnelle Auslieferung, weil man davon ausgehen kann, dass eine Pipeline nur wenig ausgeben wird.

Man könnte mal den ersten Filter mit und ohne Pufferbeschränkung ausführen und mit strace Ausgaben in eine Trace-Datei schreiben lassen. Ggf. kann man da schon sehen, was die Programme da treiben bzw. ob sie z.B. den Typ des ausgehenden Dateideskriptors abfragen.

ChickenLipsRfun2eat schrieb:

/ Nachtrag: Das mit „in einem Rutsch“ ist auch logisch, da head ja auf die 10 Zeilen wartet.

Nein, das ist nicht logisch: im Gegensatz zu tail kann head gleich mit der Ausgabe beginnen, sobald sie kommt. Da muss nicht auf zehn Zeilen gewartet werden.

Das sleep ist da schon vorbei, das gilt ja nur während der Erzeugung.

Ich glaube, Du bist hier auf einem Holzweg: es wird ja parallel ausgeführt und nicht etwa sequentiell. Wie man sieht, setzt sich ja das Timingverhalten durch die Pipeline fort, wenn man lediglich einen Filter dranhängt.

ChickenLipsRfun2eat schrieb:

Ich versuche es nochmal im Klartext, weil ich mir nicht sicher bin, ob die Aussage klar wird 😉

Hm...

Wenn du x | head -n10 machst, wird die komplette Ausgabe von x nach 10 Zeilen abgeschnitten.

Korrekt.

Wenn du x | grep | head machst, wirft grep erst nach Beenden das EOF, was nach abarbeiten der while-Schleife kommt.

Schon, aber das macht hier keinen bemerkbaren Unterschied, weil die Quelle sowieso nur 92 Bytes liefert. Das ist weit weniger als eine Speicherseite von 4k. Mit anderen Worten: sowohl head als auch egrep werden hier gleichermaßen Puffern und das "frühe" EOL von head macht keinen Unterschied für das Tröpfeln. Es sollte allerdings einen machen, wann das Ergebnis ansteht, nämlich (34 - 10) * 0.1s = 2.4s früher. Und in der Tat:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ time ./quelle | ./f1 | ./f2
2
4
6
8
12
14
16
18
20
21

real	0m3,453s
user	0m0,039s
sys	0m0,023s
$ time ./quelle | ./f2 | ./f1
2
4
6
8

real	0m1,021s
user	0m0,020s
sys	0m0,009s

In beiden Fällen erfolgt allerdings die Ausgabe in einem Rutsch. Nebenbei sieht man hier auch sehr schön, dass bestimmte Filterkombinationen je nach Reihenfolge zu unterschiedlichen Ergebnissen führen. ☺

Der Effekt vom frühen EOL wird erst so richtig sichtbar, wenn man als Quelle sehr viele Daten schickt. Das kann man z.B. ausprobieren mit seq -f '%1000f' 1 1000000:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ time seq -f '%1000f' 1 1000000
... tonnenweise Ausgabe ...
real	2m2,744s
user	0m4,164s
sys	0m8,459s
$ time seq -f '%1000f' 1 1000000 | head
... wenig Ausgabe ...
real	0m0,003s
user	0m0,000s
sys	0m0,004s

Aber, wie gesagt, das beeinflusst die Gesamtzeit, aber nicht, wie die Daten über die Zeit verteilt ausgegeben werden ("Tröpfeln").

user_unknown

(Themenstarter)
Avatar von user_unknown

Anmeldungsdatum:
10. August 2005

Beiträge: 17432

Schön, schön, und doch nicht schön.

1
./p1.sh | stdbuf -i0 -o0 -e0 egrep  "[2468]" | head 

Schön, ChickenLipsRfun2eat, das funktioniert und war mir bislang völlig unbekannt.

  • in-

  • out-

  • errorbuf nehme ich an, die manpage ist mein Freund. 😉

rklm schrieb:

ChickenLipsRfun2eat schrieb:

/ Nachtrag: Das mit „in einem Rutsch“ ist auch logisch, da head ja auf die 10 Zeilen wartet.

Nein, das ist nicht logisch: im Gegensatz zu tail kann head gleich mit der Ausgabe beginnen, sobald sie kommt. Da muss nicht auf zehn Zeilen gewartet werden.

Auch schön, dass Du, rklm, das richtig aufgefasst hasts. Grep kann zeilenweise erkennen, ob da eine 2,4,6 in der Zeile steckt, und head kann die 9 ersten Zeilen durchlassen, sobald sie kommen und wenn man nur eins von beiden benutzt klappt es ja auch.

Sed kann ja auch grep vertreten, so dass man sich auf das -u beschränken kann:

1
./p1.sh | sed -u 10q | sed -urn "/[02468]/p"

Nicht so schön: Ich kam überhaupt auf das Problem/die Idee, weil ich Werbung für Pipelines machen wollte und schon die 2 Königsargumente parat hatte: Mehrere Prozesse können parallel arbeiten und wenn einer davon sagt "Leute, wir sind fertig!" (head), dann müssen die anderen nicht sinnlos weiterarbeiten (Zahlen produzieren, Zahlen filtern). Das ist zwar weiterhin richtig, aber kein automatischer Effekt des Pipelining. Man muss sich selten genutzte Schalter merken (-u/--line-buffered/stdbuf -{i,e,o}0) - bis man die ausgekramt hat ist wohl meist auch ein sequenzielles Programm fertig.

Aber gut, wieso es nicht reibungslos klappt ist jetzt grob geklärt. Danke für die Hilfe.

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 12527

user_unknown schrieb:

Nicht so schön: Ich kam überhaupt auf das Problem/die Idee, weil ich Werbung für Pipelines machen wollte und schon die 2 Königsargumente parat hatte: Mehrere Prozesse können parallel arbeiten

Tun sie ja auch.

und wenn einer davon sagt "Leute, wir sind fertig!" (head), dann müssen die anderen nicht sinnlos weiterarbeiten (Zahlen produzieren, Zahlen filtern).

Auch das passiert ja.

Das ist zwar weiterhin richtig, aber kein automatischer Effekt des Pipelining.

Doch, natürlich. Aber wenn die Datenmenge so klein ist, dass sie komplett in die Puffer passt, dann wird halt immer nur an einer Station gearbeitet. Und das ist vermutlich sogar effizienter als kleine Pakete (z.B. Zeilen) zu nehmen und parallel daran zu arbeiten. Die Transaktionskosten (IO, Kontextwechsel) werden dann vermutlich negativ zu Buche schlagen.

Man muss sich selten genutzte Schalter merken (-u/--line-buffered/stdbuf -{i,e,o}0) - bis man die ausgekramt hat ist wohl meist auch ein sequenzielles Programm fertig.

Naja, im Regelfall (also mit mehr Daten) ist der größere Puffer schon deutlich effizienter. Dass man Daten scheller sehen will (also, sobald sie über die Pipe eintrudeln), würde ich eher als einen Spezialfall betrachten. Es ist eine übliche Abwägung: entweder will man schnell die ersten Ergebnisse sehen oder man will schnell das Gesamtergebnis haben - also, Zeit zum Empfang des ersten Bytes vs. des letzten Bytes.

Antworten |