Open Source im professionellen Einsatz
Linux-Magazin 12/2006
© photocase.com

© photocase.com

Workshop: Python-Programme optimieren

Gut gezielt

Jeder wünscht sich Programme, mit denen er flink arbeiten kann. Schnelle Software entsteht aber nicht durch flächendeckende, sondern durch wenig Optimierung - an den richtigen Stellen. Dieser Workshop zeigt, wie Sie bei Python-Programmen ins Schwarze treffen.

953

Die Optimierung von Programmen will in erster Linie Laufzeit sparen. Die Software soll sich zügiger anfühlen, ihre Rechenergebnisse schneller präsentieren und so fort. Doch leider verlängert Optimierung die Entwicklungszeit. Der entstehende Quelltext ist normalerweise komplizierter als vorher, was den Aufwand zum Testen und zur Fehlersuche steigert. Doch das ist noch nicht alles: Durch die gestiegene Komplexität des Code werden auch dessen Wartung und zum Beispiel nachträgliche Erweiterungen schwieriger. Das kostet weitere Entwicklungszeit, was oft übersehen wird, weil sie mitunter erst lange nach der Optimierung fällig sind.

Deshalb ist es falsch, schon beim Schreiben jeder Funktion oder Methode darüber nachzudenken, wie sie am schnellsten arbeiten könnte. Diese Einstellung bringt C. A. R. Hoare - obwohl der Satz häufig Donald Knuth zugeschrieben wird - mit "Premature optimization is the root of all evil" auf den Punkt. Das ist der Fall: Der Programmierer sollte die Laufzeitoptimierung erst angehen, wenn das Programm fehlerfrei läuft.

Wo optimieren?

Ein Programm ist aus Sicht des Entwicklers nie durchweg langsam oder durchweg schnell. Für träges Verhalten ist normalerweise nur ein kleiner Teil des Code verantwortlich, die so genannten Flaschenhälse (Bottlenecks). Bevor Sie anfangen, auf Verdacht Codeteile zu beschleunigen, sollten Sie messen, wo es genau klemmt. Im ersten Schritt ist festzustellen, ob die Software beim Ausführen einer bestimmten Funktion durch die CPU oder das EA-Subsystem behindert wird. So ist es sinnlos, einen Algorithmus auf ein Hundertstel seiner ursprünglichen Rechenzeit zu bringen, wenn tatsächlich die Festplatten oder das Netzwerk die Software bremsen.

Um zu ermitteln, ob der langsame Prozessor, die Festplatte oder andere Hardware die Schuldigen sind, helfen Ihnen grafische Werkzeuge wie Xosview [1] oder Gkrellm [2]. Daneben gibt es Tools wie Dstat [3], die beispielsweise den Datentransfer von und zu einer bestimmten Partition in Zahlen wiederspiegeln. Um plausibel zu messen, dürfen allerdings möglichst keine anderen Prozesse laufen, die den Rechner auslasten. Alternativ liefern »top« oder »ps« einzelne Leistungsdaten von Prozessen.

Die Abbildungen 1 bis 3 zeigen Screenshots von Gkrellm und Dstat für einen durch die CPU und einen durch Datentransfer begrenzten Prozess. Der zweite Fall ist schwieriger zu erkennen als die CPU-Begrenzung, da es keine Hardware-unabhängige Obergrenze gibt. Ermitteln Sie entsprechende Grenzwerte am besten durch Hardware-Spezifikationen oder Benchmarks. Bedenken Sie, dass Datentransfer sich nicht nur wie hier auf ein CD-ROM-Laufwerk beziehen kann, sondern auch auf Netzwerk-Schnittstellen, Tape-Wechsel-Roboter in Backup-Systemen und so weiter.

Wird die Software durch eine vollständige CPU-Auslastung gehemmt, müssen Sie die bremsenden Stellen im Code weiter eingrenzen. Dabei hilft das Python-Modul »cProfile« (bis Python 2.4 »profile«), dessen Ergebnisse das Modul »pstats« auswertet.

Abbildung 1: Links zeigt Gkrellm die hundertprozentige Prozessorauslastung für einen von der CPU-begrenzten Prozess. Rechts stellt das Programm den Datendurchsatz beim Kopieren einer CD-ROM dar.

Abbildung 2: Ausgabe von »dstat« für einen CPU-begrenzten Prozess. Die Summe der beiden ersten Spalten liegt bei 100 Prozent.

Abbildung 3: Dstat-Report für einen durch Datentransfer begrenzten Prozess (Kopieren einer CD-ROM). Wichtig sind die Werte der Spalte »disk/total/read«.

Profiling-Beispiel: Emerge

Als praktisches Beispiel für den Einsatz des Python-Profilers soll eine Suche in der Paket-Datenbank von Gentoo Linux dienen. Das entsprechende Tool »emerge« ist in Python geschrieben. Eine Suche mit »emerge --search python« läuft auf dem Rechner des Autors in einer akzeptablen Zeit von knapp 10 Sekunden. Eine Optimierung wäre also höchstens für langsamere Rechner sinnvoll, aber zunächst soll es hier ja nur um die Analyse gehen.

Die Ablaufstatistik mit Pythons C-Profile-Modul zu erzeugen ist etwas umständlich, da dessen Kommandozeilenschnittstelle nicht vorsieht, dem aufgerufenen Programm Parameter mitzugeben. Daher verwendet der Workshop den interaktiven Interpreter (Listing 1). Nach dem Importieren der Python-Module und der Vorbereitung der Parameter startet »cProfile.run()« den Testlauf. Das Modul »pstats« gibt dann die Laufzeitstatistik tabellarisch aus (Listing 2).

Listing 1:
»cProfile«

01 >>> import cProfile
01 >>> import sys
01 >>> sys.argv.append("--search")
01 >>> sys.argv.append("python")
01 >>> f = open("/usr/bin/emerge")
01 >>> ef = f.read()
01 >>> f.close()
01 >>> cProfile.run(ef, "emerge.stats")
01 Searching...
01 [ Results for search key : python ]
01 [ Applications found : 48 ]
01 ...

Listing 2:
»pstats«

01 >>> import pstats
01 >>> s = pstats.Stats("emerge.stats")
01 >>> s.sort_stats('time')
01 <pstats.Stats instance at 0xb7d80eac>
01 >>> s.print_stats(10)
01 Sun Oct  1 23:12:36 2006    emerge.stats
01 
01          602508 function calls (586701 primitive calls) in 9.052 CPU seconds
01 
01    Ordered by: internal time
01    List reduced from 609 to 10 due to restriction <10>
01 
01    ncalls  tottime  percall  cumtime  percall filename:lineno(function)
01      1240    1.022    0.001    1.022    0.001 {method 'readlines' of 'file' objects}
01     11387    0.849    0.000    0.849    0.000 {method 'flush' of 'file' objects}
01      1096    0.579    0.001    1.513    0.001 /usr/lib/portage/pym/portage.py:200(cacheddir)
01 14550/160    0.421    0.000    1.173    0.007 /home/schwa/python2.5/lib/python2.5/copy.py:144(deepcopy)
01     76352    0.359    0.000    0.359    0.000 {method 'append' of 'list' objects}
01         1    0.335    0.335    2.513    2.513 <string>:468(output)
01 66288/66173    0.316    0.000    0.317    0.000 {len}
01     11383    0.256    0.000    1.225    0.000 <string>:94(update_twirl)
01     36953    0.224    0.000    0.224    0.000 {method 'split' of 'str' objects}
01         1    0.213    0.213    3.629    3.629 <string>:408(execute)

Ein besonderer Flaschenhals fällt zwar nicht auf, dennoch sind ein paar Stellen zu erkennen, an denen eine Optimierung ansetzen könnte. So verbringt Emerge immerhin 1,2 von 9 Sekunden damit, die Fortschrittsanzeige (Methode »update_twirl«) zu aktualisieren (zu sehen in der Spalte »cumtime«, kumulative Zeit, in der achten Tabellenzeile). Allerdings gibt es bereits eine Emerge-Option, die diese Ausgabe abschaltet.

Etwa 1,2 Sekunden fallen für tiefe Kopien an (Spalte »cumtime« in der vierten Tabellenzeile). Tiefe Kopien werden von Programmierern manchmal einfach sicherheitshalber gemacht. Hier gibt es Einsparpotenzial, falls die tiefe Kopie nicht wirklich nötig ist.

Möglicherweise ließe sich der Code nach einer Umstrukturierung noch besser beschleunigen, da dann deutlicher zu erkennen wäre, wann welcher Code abläuft und welche algorithmischen Vereinfachungen möglich sind. Schließlich ist die Änderung von Algorithmen oft die wirksamste Laufzeitoptimierung.

Linux-Magazin kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

  • Python 3.4

    Die Entwickler haben Python 3.4 an zahlreichen Ecken optimiert und die Standardbibliothek um einige interessante Module erweitert, findet Python-Experte Mike Müller.

  • Python 3.4.0 mit Pip-Integration und neuen Modulen

    Seit gestern ist Python 3.4.0 verfügbar. Zu den Highlights der neuen Version zählen der Pip-Support und neue Module für Statistiken und asynchrone Eingabe-Ausgabe-Operationen.

  • Haskell

    In der funktionalen Programmiersprache Haskell lässt sich bemerkenswert kompakter Code schreiben, der dennoch leicht zu lesen ist. Dieser Artikel erklärt, welche Sprachkonstrukte und -eigenschaften diese Meisterleistung ermöglichen. Eine Code-Besichtigung für Haskell-Interessierte.

  • Funktionale Programmierung (1): Grundzüge

    Ob Microsofts F# oder neue Features in C++, Java und Python: Der funktionale Programmierstil macht wieder Schlagzeilen. Mit diesem Online-Artikel beginnt Linux-Magazin Online eine kleine Reihe zur funktionalen Programmierung. In der ersten Folge beschreibt unser Autor Rainer Grimm die Grundzüge dieses Programmierparadigmas. Kommende Folgen widmen sich der funktionalen Programmierung in Python und dem Framework MapReduce von Google.

  • Leserbriefe

    Haben Sie Anregungen, Statements oder Kommentare? Dann schreiben Sie an [redaktion@linux-magazin.de]. Die Redaktion behält es sich vor, die Zuschriften und Leserbriefe zu kürzen. Sie veröffentlicht alle Beiträge mit Namen, sofern der Autor nicht ausdrücklich Anonymität wünscht.

comments powered by Disqus

Ausgabe 04/2017

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.