Fortschritt. Oder nicht? //2289

Auch kurz vor dem Jahresende gibt es noch einiges in der Firma zu tun.
Ich hatte einem meiner Mitarbeiter aufgetragen, eine Funktion zu schreiben, die aus einer gegebenen Liste von ein paar Tausend Dateien den jeweiligen MD5-Hash (ja, ich weiß, dass der MD5 nicht mehr als sicher gilt, es geht hier aber überhaupt nicht um einen sicherheitskritischen Zweck, sondern darum, die Dateiinhalte eindeutig unterscheiden zu können) ermittelt.
Die Funktion des Mitarbeiters lieferte korrekte Ergebnisse, aber die Performance war mies. Man musste fast minutenlang warten. Also schauten wir, ob wir nicht einen schnelleren Algorithmus finden würden. Er hatte die Grundlage für den Code für die Hashberechnung im Internet gefunden und angepasst. Ich erinnerte mich, dass ich für entsprechende Berechnungen immer eine API-Funktion des Betriebssystems genutzt hatte. Ob die allerdings merklich schneller laufen würde, wusste ich auch nicht.
Gemeinsam mit dem Mitarbeiter stellten wir den Code um. Und tatsächlich – in unserem Testsystem lief die Funktion deutlich schneller. Das heißt natürlich nicht, dass sie das immer tut. Wenige große Dateien können sich anders verhalten als viele kleine, auch wenn die Gesamtdatenmenge übereinstimmt.
Ich trug dem Mitarbeiter auf, noch ein wenig mit anderen Szenarien zu testen, aber vermutlich wird es bei der API-Funktion bleiben.

Perspektivenwechsel: Bestimmt hat jeder von euch sich schon einmal geärgert, oder zumindest gewundert, wenn eine Fortschrittsanzeige (z.B. bei einer Installation) so überhaupt nicht zu passen scheint. Da hängt der Balken minutenlang, dann wieder macht er einen Sprung, die prognostiziert verbleibende Zeit wird immer länger statt kürzer, um irgendwann, wenn man es schon fast aufgegeben hat, ganz plötzlich fertig zu sein. Warum ist das so? Warum läuft der Fortschrittsbalken nicht einfach gleichmäßig und stetig durch, und die verbleibende Zeit nimmt linear ab?
Man muss sich mal überlegen, wie man selbst solche Fortschritte berechnen würde. Bei komplexen Aufgaben wie einer Installation kommen viele Einzeltasks zusammen (z.B. Registryeinträge), die alle unterschiedlich lange dauern können, und das abhängig vom jeweiligen System. In unserem Beispiel beschränken wir uns deshalb auf den Use Case, dass mehrere Gigabyte Daten, aufgeteilt in tausende Dateien innerhalb hunderter verschachtelter Verzeichnisse umkopiert werden sollen.
Wenn dafür das Source-Verzeichnis angegeben wurde, weiß man zunächst nicht, wie viele Dateien überhaupt enthalten sind, und wie groß sie sind. Natürlich könnte man in einem ersten Schritt, die Verzeichnisstruktur rekursiv durchlaufen, um zunächst die Dateien zu zählen und ihre Größen aufzusummieren. Das dauert aber natürlich auch, und bringt eigentlich nichts, außer etwas zuverlässigere Informationen für den Nutzer. Möglich wäre sogar, dass sich die Dateien zwischenzeitlich ändern. Je nach intended use kann man das also vorher machen, oder doch lieber nicht.
(Die Möglichkeit, die einzelnen Aufgaben in mehrere unabhängige Threads aufzuteilen, ignorieren wir hier mal.)

In erster Näherung kann man dann den Ansatz für die insgesamt benötigte Zeit t_ges machen: t_ges = t_0 + t_N*N + t_S*S, t_0 ein Offset, der grundsätzlich benötigt wird, t_N die Zeit pro Datei, t_S die Zeit pro Informationseinheit, N die Anzahl der Dateien, und S die Gesamtinformationsmenge der zu bearbeitenden Dateien.
Wer es noch genauer braucht, kann in einem weiteren Summanden t_D*D auch noch die Anzahl der Verzeichnisse D berücksichtigen. Das dürfte aber in den meisten Fällen nicht viel bringen, und den Aufwand noch weiter erhöhen. Höhere Potenzen noch hinzuzunehmen, dürfte erst recht nicht lohnen.
Nachdem das Programm die ersten Dateien übertragen hat, kann es aufgrund der gemessenen Zeiten unter Benutzung des obigen Ansatzes die Parameter t_? berechnen. Bei drei Unbekannten braucht es dafür drei voneinander unabhängige Zeitpunkte.
Diese Parameter ermöglichen dann eine erste Abschätzung, die dem Nutzer angezeigt werden kann.

Gerade beim Kopieren, aber ebenso bei anderen Vorgängen, hängt die benötigte Zeit auch davon ab, was der Computer sonst noch gleichzeitig macht, und wie weitere Resourcen ausgelastet sind – etwa die Netzwerkbandbreite beim Zugriff auf einen Share. Oder auch, ob benötigte Daten bereits im Cache oder sonstwo vorhanden sind. Die Geschwindigkeit kann also variieren. Das heißt, die genannten Parameter t_? sind nicht konstant, sondern können sich verändern.
Es empfiehlt sich deshalb, die Zeitmessung nicht von Anfang an in die Berechnung einfließen zu lassen, sondern nur die letzten vielleicht fünf Minuten, oder was angesichts der konkreten Aufgabenstellung als zweckmäßig erscheint.

Ihr merkt schon – das kann beliebig kompliziert werden. Ich belasse es deshalb bei diesem kleinen Einblick, was man alles berücksichtigen müsste. Es ist hoffentlich klar geworden, warum eine Fortschrittsanzeige grundsätzlich nicht zuverlässig funktionieren kann, und man sich verlässlichen Ergebnissen nur ungefähr annähern kann.
Wenn ihr euch das nächste Mal wieder über eine merkwürdig erscheinende Fortschrittsanzeige wundert, erinnert euch hieran.

Über Anne Nühm (breakpoint)

Die Programmierschlampe.
Dieser Beitrag wurde unter Uncategorized abgelegt und mit , , , , verschlagwortet. Setze ein Lesezeichen auf den Permalink.

8 Antworten zu Fortschritt. Oder nicht? //2289

  1. Leser schreibt:

    Ich habe bisher vor allem beobachtet, dass Programme, die beim Kopieren von Dateien etwas präzisere Informationen ausgeben, wie z.B. der mc, in der Regel einen einfachen Dreisatz machen, d.h. die Restzeit wird als die restlich zu kopierende Datenmenge geteilt durch den aktuellen Datendurchsatz angegeben. Wobei der Datendurchsatz bei Kopiervorgängen von unterschiedlichen Faktoren abhängt, deren hervorstechendster (für mich subjektiv) das Verhältnis Anzahl zu Größe ist, d.h. es macht einen Unterschied, ob ich 300.000 Dateien à wenige kB habe, oder 3 Dateien a 1GB. Das kann natürlich an verschiedensten Faktoren liegen, wie z.B. der verwendeten elektronischen Schnittstelle des Laufwerks (intern heutzutage meist SATA, aber auch teilweise schon NVMe, extern USB2 oder USB3, und kann der USB Bus auch in der Konstellation mit diesen Geräten USB3 Geschwindigkeiten erreichen), der Geschwindigkeit des Laufwerks selbst (Flash Speicher oder HDD), sowie den verwendeten Dateisystemen überhaupt.

    Gefällt 1 Person

  2. mindph schreibt:

    Ich hatte damals unter Win98 regelmäßig so das Phänomen, dass das Kopieren größerer Mengen an Samples zwei Stunden lang nur „noch 30 Minuten“ dauerte. Ich glaube, Microsoft hat bis Win10 gebraucht, um halbwegs vernünftige Anzeigen hinzukriegen…

    Anyhow, warum macht ihr das Hashing nicht mit md5sum in einem Shellscript? Und kopieren dann mit rsync (statt cp)?

    Gefällt 1 Person

    • Anlass zu diesem Post hat u.a. das Verhalten von W10 gegeben, wo mir das mit der merkwürdigen Anzeige erst kürzlich wieder bei einem Defender-Scan aufgefallen war.

      Die Verarbeitung der Daten (also u.a. Generierung der Dateiliste, Berechnung und Weiterbenutzung der Hashes) findet innerhalb einer lokalen SW-Anwendung statt.
      Die Dateiliste erst umständlich zu exportieren, ein Script o.ä. drüberlaufen zu lassen, und die Hashes dann wieder einzulesen, bringt nichts.

      Like

  3. Engywuck schreibt:

    Das mit dem „Betriebssystem-Funktion ist schneller“ kann daran liegen, dass das Betriebssystem mehrere Varianten parallel mitbringt (evtl. gar handoptimierter Assembler) und dann jeweils die verwendet, die am besten zum Prozessor passt (MD5 soll recht gut von SSE2 profitieren).
    Man sollte ohnehin immer mit API-Funktionen anfangen – muss man schon nicht selber pflegen und Implementierungs-Fehler sind unwahrscheinlicher. Außer natürlich, man wird als Programmierer nach Lines of Code bezahlt 🙂
    (Apropos dazu: https://www.folklore.org/StoryView.py?story=Negative_2000_Lines_Of_Code.txt )

    Gefällt 1 Person

  4. Pingback: Zwitscherlinge //2411 | breakpoint

Hinterlasse einen Kommentar