Programmierpraktikum SoSe 2024, Bachelor Informatik, FU Berlin
ProPra2024 > Bibliotheken > Python-Standardbibliothek > m_subprocess2

subprocess: Unterprozess starten, um ein Speicherlimit zu lösen

Experience

Ziel

Ich kann Teilaufgaben an einen Unterprozess delegieren, um tolle Fähigkeiten existierender Nicht-Python-Software zu nutzen, anstatt in Python großen Aufwand zu treiben.

Hintergrund

Manchmal scheitert ein naiv geschriebenes Pythonprogramm katastrophal an einer Leistungsanforderung, die ein gekonnt geschriebenes mit nur wenigen zusätzlichen Programmzeilen lässig hinbekommt. So ein Beispiel turnen wir in dieser Aufgabe durch. Nicht ganz einfach, aber mit hohem Fun-Faktor.

Das Beispiel basiert darauf, dass man den Median einer Sequenz von Werten am einfachsten so ausrechnet, dass man die Sequenz sortiert und dann das mittlere Element herausholt. Doch was, wenn die Sequenz so lang ist, dass sie nicht in den Speicher passt?

Tricky

Arbeitsschritte

Falls Sie sich noch nie näher mit Debugging beschäftigt haben, empfiehlt es sich, sich darin vor dieser Aufgabe ein Rüstzeug zuzulegen.

Programm, Teil 1

  • 1 Legen Sie das Programm m_subprocess2.py an und realisieren Sie darin die folgenden Funktionen:
  • 2 initrandom()
    initialisiert den Zufallsgenerator mit dem Wert 42, damit jeder Programmlauf dasselbe Ergebnis bringt.
  • 3 random_line() -> str
    erzeugt eine Zeile (mit "\n" am Ende) die in Hexadezimalschreibweise die SHA512-Prüfsumme von 4 mit einem einzigen random-Aufruf erzeugten Zufallsbytes enthält.
  • 4 random_lines_generator(n: int) -> collections.abc.Iterator[str]:
    ist eine Generator-Funktion, deren Iterator n solche random_line()-Zeilen liefert.
  • 5 sorted_lines_generator_python(n: int) -> collections.abc.Iterator[str]:
    ist eine darauf aufbauende Generator-Funktion, deren Iterator n solche Zeilen liefert, die mittels sorted sortiert wurden.
  • 6 median(numlines: int, sorted_lines_iterator: collections.abc.Iterator[str]) -> str:
    berechnet den Median von numlines Zeilen, die von sorted_lines_iterator bereits in sortierter Reihenfolge bereitgestellt werden. Bei einer ungeraden Zahl von Zeilen ist das die mittlere Zeile, bei einer geraden Zahl ist es die Zeile nach der Mittellücke.

Tests zu Teil 1

Schreiben Sie nun ein paar zugehörige Unittests direkt mit in die gleiche Datei:

  • 7 test_random_line():
    stellt sicher, dass die erste erzeugte Zufallszeile auf "2ed9f9\n"" endet.
  • 8 test_random_lines():
    stellt sicher, dass bei 4 mittels random_lines_generator erzeugten Zufallszeilen die erste mit "6e9a55"" beginnt und die letzte mit "0f757a\n"" endet.
  • 9 def test_median4():
    stellt sicher, dass der Median von 4 Zufallszeilen mit "72f436" beginnt und mit "d68c77\n" endet.
  • 1 Zeigen sie einen erfolgreichen Lauf von pytest -v m_subprocess2.py.

Hauptprogramm dazu

  • 10 Ergänzen Sie unten in derselben Datei ein Hauptprogramm, das man mit 2 Kommandozeilenargumenten aufruft: Argument 1 ist entweder "local" oder "print"; Argument 2 ist eine ganze Zahl n.
    Bei "local" gibt das Programm das Ergebnis von median(n, sorted_lines_generator_python(n)) aus.
    Bei "print" gibt es die Ergebnisse von random_lines_generator(n) aus.
  • 2 Zeigen sie python m_subprocess2.py print 4.
  • 3 Zeigen sie python m_subprocess2.py local 4.

Speicherbeschränkung

Nun kommen wir zum Thema. Damit wir nicht Ewigkeiten warten müssen, bis der Speicher eines modernen Rechners wirklich ganz gefüllt ist, machen wir uns eine Speicherbeschränkung auf 512 Megabyte künstlich.

  • 4 Zeigen sie python m_subprocess2.py local 5000000.
  • Lesen Sie sich das bash-Kommando ulimit an.
  • 5 Zeigen Sie ein Kommando, das mittels ulimit den virtuellen Speicher auf 512.000 Kilobytes beschränkt und dann python m_subprocess2.py local 5000000 aufruft.
    Dieser Aufruf sollte mit "MemoryError" fehlschlagen, denn diese Daten passen nicht mehr alle in die 512 MB hinein. (Diese Fehlschläge beginnen irgendwo zwischen 2000000 und 3000000. Der genaue Wert hängt von vielen Einzelheiten ab.)

Programm, Teil 2

Nun bauen wir eine Variante die trotz der Speicherbeschränkung auf 512 MB noch immer 5 Millionen Zeilen sortieren kann.

  • 11 Ergänzen Sie Ihr Programm um folgende Funktion:
    sorted_lines_generator_subprocess(n: int) -> collections.abc.Iterator[str]:
    Diese hat von außen gesehen die gleiche Funktionalität wie sorted_lines_generator_python, aber sie benutzt zum Sortieren das Modul subprocess, um das Unix-Utility sort aufzurufen.
    Sie schreibt dabei die Zeilen direkt (also ohne Zwischendatei) über eine Pipe in den sort-Prozess und liest die sortierten Ergebnisse ebenfalls direkt, also wieder ohne Zwischendatei, sondern ebenfalls über eine Pipe aus dem sort-Prozess.
  • 12 Ergänzen Sie Ihr Hauptprogramm so, dass das erste Argument auch "subprocess" lauten darf und dann sorted_lines_generator_subprocess statt sorted_lines_generator_python verwendet wird.
Hinweis (nur bei Bedarf): Debugginghilfe

Wenn man sich vertut, wird die Problemsuche schnell unübersichtlich, wenn man mit Unterprozessen arbeitet. Folgende Testausgaben sind vermutlich hilfreich, jedenfalls wenn Sie sie nach sys.stderr schreiben:

  • in random_lines_generator summieren Sie die Länge der erzeugten Zeilen auf und machen jeweils nach 200000 Ergebnissen eine Testausgabe der Form
    line 200000: 24.6 MB total output
  • in median machen Sie jeweils nach 200000 Ergebnissen eine Testausgabe der Form
    have read 200k lines

Tests zu Teil 2

  • 13 Ändern Sie den existierenden Test def test_median4() auf folgende Signatur ab:
1
2
@pytest.mark.parametrize("generator", [sorted_lines_generator_python(4), sorted_lines_generator_subprocess(4)])
def test_median(generator):
  • Das soll dieselbe bisherige Testlogik mit den angegebenen zwei verschiedenen Iteratoren ausführen, wobei jede der Varianten separat fehlschlagen kann und selbständig berichtet wird.
  • 6 Zeigen sie einen erfolgreichen Lauf von pytest -v m_subprocess2.py.
  • 14 Lesen Sie den obigen Hinweis und ergänzen Sie die Debuggingausgaben genau wie dort beschrieben, um die Kontrolle Ihres Ergebnisses zu erleichtern.

Voilá!

  • 7 Zeigen Sie ein Kommando, das mittels ulimit den virtuellen Speicher auf 512.000 Kilobytes beschränkt und dann python m_subprocess2.py subprocess 5000000 aufruft.
    Dieser Aufruf sollte erfolgreich sein und das korrekte Resultat "800aea48b0…" liefern, obwohl diese Daten nicht mehr alle in die 512 MB hineinpassen.

Erstaunlich, oder?

  • 1 Aber warum funktioniert das? Der Unterprozess unterliegt doch derselben Speicherbeschränkung auf 512 MB, in der unsere Daten nicht genug Platz finden!
    Recherchieren Sie, wie GNU sort das anstellt, und geben Sie für die Antwort eine möglichst vertrauenswürdige Quelle an.
Program
Trace
Information

Abgabe

Geben Sie den Quellcode ab, wie er am Ende der Aufgabe vorliegt.

Geben Sie ein Kommandoprotokoll ab, das genau nur die Eingaben und Ausgaben der obigen Kommandos 1, 2, … enthält. Entfernen Sie vor Abgabe eventuelle Fehlversuche und sonstige zusätzliche Kommandos aus dem Protokoll.

Geben Sie ein Markdown-Dokument ab mit knappen Antworten zu den oben gestellten Fragen 1, 2, … Geben Sie diese Marker mit an.
Geben Sie ggf. Beispiele oder benutzte Quellen an.