.. SPDX-FileCopyrightText: 2021 cusy GmbH
..
.. SPDX-License-Identifier: BSD-3-Clause
Performance
===========
Mit Python lässt sich zwar schnell Code schreiben und testen, da es eine
interpretierte Sprache ist, die dynamisch typisiert. Dies sind jedoch auch die
Gründe, die sie bei der wiederholten Ausführung von einfachen Aufgaben,
:abbr:`z.B. (zum Beispiel)` in Schleifen, langsam macht.
Bei der Entwicklung von Code kann es häufig zu Kompromissen zwischen
verschiedenen Implementierungen kommen. Zu Beginn der Entwicklung eines
Algorithmus ist es jedoch meist kontraproduktiv, sich um die Effizienz des Codes
zu kümmern.
»Wir sollten kleine Effizienzsteigerungen in sagen wir etwa 97 % der Zeit,
vergessen: Vorzeitige Optimierung ist die Wurzel allen Übels. Dennoch
sollten wir unsere Chancen in diesen kritischen 3 % nicht verpassen.«[#]_
.. [#] Donald Knuth, Begründer des `Literate programming
`_, in Computer Programming as an
Art (1974)
.. seealso::
* `Speed up your data science and scientific computing code
`_
k-Means-Beispiel
----------------
Im Folgenden zeige ich Beispiele für den `k-Means-Algorithmus
`_, um aus einer Menge von
Objekten eine vorher bekannte Anzahl von Gruppen zu bilden. Dies lässt sich mit
dem `MacQueen’s Algorithmus
`_ in
den folgenden drei Schritten erreichen:
#. Wähle die ersten :samp:`k` Elemente als Clusterzentren
#. Weise jedes neue Element dem Cluster zu, bei dem sich die Varianz am
wenigsten erhöht
#. Aktualisiere das Clusterzentrum
Die Schritte 2 und 3 werden dabei so lange wiederholt, bis sich die Zuordnungen
nicht mehr ändern.
Eine mögliche Implementierung mit reinem Python könnte so aussehen:
.. literalinclude:: py_kmeans.py
:caption: py_kmeans.py
:name: py_kmeans.py
:lines: 6-
Beispieldaten können wir uns erstellen mit:
.. code-block:: python
from sklearn.datasets import make_blobs
points, labels_true = make_blobs(
n_samples=1000, centers=3, random_state=0, cluster_std=0.60
)
Und schließlich können wir die Berechnung ausführen mit:
.. code-block:: python
kmeans(points, 10)
Performance-Messungen
---------------------
Wenn ihr erst einmal mit eurem Code gearbeitet habt, kann es nützlich sein, die
Effizienz genauer zu untersuchen. Hierfür kann :abbr:`z. B. (zum Beispiel)`
:doc:`cProfile `, :doc:`ipython-profiler`, :doc:`scalene`, :doc:`tprof`
oder :doc:`memray` genutzt werden. Bisher führe ich meist die folgenden Schritte
aus:
#. Ich profiliere das gesamte Programm mit :doc:`cProfile ` oder
`py-spy `_, um langsame Funktionen zu
finden.
#. :abbr:`Ggf (Gegebenenfalls)` finde ich mit dem `line_profiler
`_ die langsamen Stellen innerhalb
der Funktion
#. Sofern die langsame Funktion rechenintensiv ist, versuche ich eine der folgen
den Optimierungen; sofern jedoch die Anwendung datenintensiv ist (Dicts,
Strings, I/O)), schaue ich mir die Architektur nochmal genauer an.
#. Schließlich erstelle ich ein neues Profil und filtere das Ergebnis meiner
optimierten Version heraus um die Ergebnisse vergleichen zu können.
.. versionadded:: Python3.15
Mit :pep:`799` wird ein spezielles Profiling-Modul zur Verfügung stehen, das
die in Python integrierten Profiling-Tools unter einem einheitlichen
Namespace organisiert. Dieses Modul enthält:
:mod:`profiling.tracing`
deterministische Funktionsaufrufverfolgung, die aus :doc:`cProfile
` verschoben wurde.
:mod:`profiling.sampling`
der neue statistische Sampling-Profiler :doc:`tachyon`.
.. seealso::
* `airspeed velocity (asv) `_ ist ein
Werkzeug zum Benchmarking von Python-Paketen während ihrer Lebensdauer.
Laufzeit, Speicherverbrauch und sogar benutzerdefinierte Werte können
aufgezeichnet und in einem interaktiven Web-Frontend angezeigt werden.
* `Python Speed Center `_
* `Tracing the Python GIL
`_
.. toctree::
:hidden:
:titlesonly:
:maxdepth: 0
tracing
ipython-profiler.ipynb
scalene.ipynb
tprof
memray
tachyon
1. Suche nach bestehenden Implementierungen
-------------------------------------------
Ihr solltet nicht das Rad neu erfinden wollen: Wenn es bestehende
Implementierungen gibt, solltet ihr diese auch verwenden. für den
k-Means-Algorithmus gibt es sogar gleich zwei Implementierungen:
* `sklearn.cluster.KMeans
`_
.. code-block:: python
from sklearn.cluster import KMeans
KMeans(10).fit_predict(points)
* `dask_ml.cluster.KMeans
`_
.. code-block:: python
from dask_ml.cluster import KMeans
KMeans(10).fit(points).predict(points)
Gegen diese bestehenden Lösungen könnte bestenfalls sprechen, dass sie einen
erheblichen Overhead in eurem Projekt erzeugen könnten wenn ihr nicht schon
an anderen Stellen `scikit-learn `_ oder
`Dask-ML `_ einsetzt. Im Folgenden werde ich daher weitere
Möglichkeiten zeigen, euren eigenen Code zu optimieren.
2. Anti-Patterns finden
-----------------------
Anschließend könnt ihr mit :doc:`perflint` euren Code durchsuchen nach den
häufigsten Performance-Anti-Patterns in Python.
.. toctree::
:hidden:
:titlesonly:
:maxdepth: 0
perflint
.. seealso::
* `Effective Python `_
3. Vektorisierungen mit NumPy
-----------------------------
:doc:`../workspace/numpy/index` verlagert sich wiederholende Operationen in eine
statisch typisierte kompilierte Schicht, um so die schnelle Entwicklungszeit von
Python mit der schnellen Ausführungszeit von C zu kombinieren.
+---------------+---------------+----------+
| Version | Spectral-norm | vs 3.14x |
+===============+===============+==========+
| CPython 3.14 | 14,046ms | |
| – Basis | | |
+---------------+---------------+----------+
| NumPy | 27ms | 520x |
+---------------+---------------+----------+
:abbr:`Evtl. (Eventuell)` könnt ihr :doc:`../workspace/numpy/ufunc`,
:doc:`Vektorisierung <../workspace/numpy/vectorisation>` und
:doc:`../workspace/numpy/indexing-slicing` in allen Kombinationen nutzen um sich
wiederholende Operationen in kompilierten Code zu verschieben und damit langsame
Schleifen zu vermeiden, :abbr:`z. B. (zum Beispiel)`:
.. literalinclude:: np_kmeans.py
:caption: np_kmeans.py
:name: np_kmeans.py
:lines: 5-12
Die Vorteile von NumPy sind, dass der Python-Overhead nur je Array und nicht je
Array-Element auftritt. Da NumPy eine spezifische Sprache für Array-Operationen
verwendet, erfordert es jedoch auch eine andere Denkweise beim Schreiben von
Code. Schließlich können die Batch-Operationen auch zu übermäßigem
Speicherverbrauch führen.
.. seealso::
* `Speeding up NumPy with parallelism
`_ by Itamar
Turner-Trauring
4. Spezielle Datenstrukturen
----------------------------
:doc:`../workspace/pandas/index`
für SQL-ähnliche :doc:`../workspace/pandas/group-operations` und
:doc:`../workspace/pandas/aggregation`.
So könnt ihr auch die Schleife in der Methode ``compute_centers`` umgehen:
.. literalinclude:: pd_kmeans.py
:caption: pd_kmeans.py
:name: pd_kmeans.py
:lines: 5-8, 16-19
`scipy.spatial `_
für räumliche Abfragen wie Entfernungen, nächstgelegene Nachbarn, k-Means
:abbr:`usw. (und so weiter)`
Unsere ``find_labels``-Methode kann dann noch spezifischer geschrieben
werden:
.. literalinclude:: sp_kmeans.py
:caption: sp_kmeans.py
:name: sp_kmeans.py
:lines: 5-14
`scipy.sparse `_
`dünnbesetzte Matrizen `_
für 2-dimensionale strukturierte Daten.
`Sparse `_
für N-dimensional strukturierte Daten.
`scipy.sparse.csgraph `_
für graphenähnliche Probleme, :abbr:`z.B. (zum Beispiel)` die Suche nach
kürzesten Wegen.
`Xarray `_
für die Gruppierung über mehrere Dimensionen hinweg.
.. toctree::
:hidden:
:titlesonly:
:maxdepth: 0
parallelise-pandas
5. Compiler wählen
------------------
Faster Cpython
~~~~~~~~~~~~~~
Auf der PyCon US im Mai 2021 stellte Guido van Rossum mit `Faster CPython
`_ ein Projekt vor, das die Geschwindigkeit
von Python 3.11 verdoppeln soll. Die Zusammenarbeit mit den anderen
Python-Kernentwicklern ist in :pep:`PEP 659 – Specializing Adaptive Interpreter
<659>` geregelt. Zudem gibt es einen offenen `Issue Tracker
`_ und diverse `Werkzeuge zum
Sammeln von Bytecode-Statistiken `_.
Von den Änderungen profitieren dürfte vor allem CPU-intensiver Python-Code;
bereits in C geschriebener Code, I/O-lastige Prozesse und Multithreading-Code
dürften hingegen kaum profitieren.
Und tatsächlich sind die cPython-Versionen seitdem deutlich performanter
geworden:
+------------------+---------+
| Version | |
+==================+=========+
| CPython 3.10.4 | 1.422x |
+------------------+---------+
| CPython 3.12.0 | 1.093x |
+------------------+---------+
| CPython 3.13.0 | 1.024x |
+------------------+---------+
| CPython 3.15.0a0 | |
| – Basis | |
+------------------+---------+
.. seealso::
* `Faster CPython
`__
* `Faster CPython Benchmark Infrastructure
`_
In einen anderen Vergleich wurde auch Free-threaded Python einbezogen:
+---------------+---------+---------+---------------+----------+
| Version | N-body | vs 3.14 | Spectral-norm | vs 3.14x |
+===============+=========+=========+===============+==========+
| CPython 3.10 | 1,663ms | 0.75x | 16,826ms | 0.83x |
+---------------+---------+---------+---------------+----------+
| CPython 3.11 | 1,200ms | 1.04x | 13,430ms | 1.05x |
+---------------+---------+---------+---------------+----------+
| CPython 3.13 | 1,134ms | 1.10x | 13,637ms | 1.03x |
+---------------+---------+---------+---------------+----------+
| CPython 3.14 | 1,242ms | | 14,046ms | |
| – Basis | | | | |
+---------------+---------+---------+---------------+----------+
| CPython 3.14t | 1,513ms | 0.82x | 14,551ms | 0.97x |
+---------------+---------+---------+---------------+----------+
– Quelle: `The Optimization Ladder
`_
Python JIT compiler
~~~~~~~~~~~~~~~~~~~
.. versionadded:: Python3.15
Der `JIT-Compiler `_
in Python 3.15 zeigt eine mittlere Leistungssteigerung von 3–4 gegenüber dem
Standard-CPython-Interpreter, wobei die Performance-Messungen schwanken
zwischen -20 und über 100 % auf x86-64-Linux- und AArch64-macOS-Systemen.
.. seealso::
* `The JIT Compiler
`_
* :ref:`whatsnew315-jit`
+------------------+---------+
| Version | |
+==================+=========+
| CPython 3.15.0a0 | 1.001x |
| (JIT) | |
+------------------+---------+
| CPython 3.15.0a0 | |
| – Basis | |
+------------------+---------+
Cython
~~~~~~
Bei intensiven numerischen Operationen kann Python sehr langsam sein, auch wenn
ihr alle Anti-Patterns vermieden und Vektorisierungen mit NumPy genutzt habt.
Dann kann das Übersetzen von Code in `Cython `_ hilfreich
sein.
+---------------+---------+---------+---------------+----------+
| Version | N-body | vs 3.14 | Spectral-norm | vs 3.14x |
+===============+=========+=========+===============+==========+
| CPython 3.14 | 1,242ms | | 14,046ms | |
| – Basis | | | | |
+---------------+---------+---------+---------------+----------+
| Cython | 10ms | 124x | 142ms | 99x |
+---------------+---------+---------+---------------+----------+
Leider muss der Code jedoch häufig umstrukturiert werden und nimmt dadurch an
Komplexität zu. Auch werden explizite Type Annotations und das Bereitstellen des
Codes umständlicher.
Unser Beispiel könnte dann so aussehen:
.. literalinclude:: cy_kmeans.pyx
:caption: cy_kmeans.pyx
:name: cy_kmeans.pyx
:lines: 5-32
.. seealso::
* `Cython Tutorials
`_
* `The Cython Minefield
`_
Numba
~~~~~
`Numba `_ ist ein JIT-Compiler, der vor allem
wissenschaftlichen Python- und NumPy-Code in schnellen Maschinencode
übersetzt, :abbr:`z.B. (zum Beispiel)`:
.. literalinclude:: nb_kmeans.py
:caption: nb_kmeans.py
:name: nb_kmeans.py
:lines: 5-29
Numba benötigt allerdings `LLVM `_ und
einige Python-Konstrukte werden nicht unterstützt.
+---------------+---------+---------+---------------+----------+
| Version | N-body | vs 3.14 | Spectral-norm | vs 3.14x |
+===============+=========+=========+===============+==========+
| CPython 3.14 | 1,242ms | | 14,046ms | |
| – Basis | | | | |
+---------------+---------+---------+---------------+----------+
| Numba | 22ms | 56x | 104ms | 135x |
+---------------+---------+---------+---------------+----------+
6. Aufgabenplaner
-----------------
:doc:`jupyter-tutorial:hub/ipyparallel/index`, :doc:`dask` und `Ray
`_ können Aufgaben in einem Cluster verteilen.
Dabei haben sie unterschiedliche Schwerpunkte:
* ``ipyparallel`` integriert sich einfach in ein
:doc:`jupyter-tutorial:hub/index`.
* :doc:`dask` imitiert pandas, NumPy, Iteratoren, Toolz und PySpark bei der
Verteilung ihrer Aufgaben.
* Ray bietet eine einfache, universelle API für den Aufbau verteilter
Anwendungen.
* `RLlib `_ skaliert insbesondere
reinforcement Learning.
* Ein `Backend für Joblib
`_ unterstützt
verteilte `scikit-learn `_-Programme.
* `XGBoost-Ray
`_
ist ein Backend für verteiltes `XGBoost
`_.
* `LightGBM-Ray
`_
ist ein Backend für verteiltes `LightGBM
`_
* `Collective Communication Lib
`_ bietet
eine Reihe von nativen Collective-Primitiven für `Gloo
`_ und die `NVIDIA Collective
Communication Library (NCCL)
`_.
Unser Beispiel könnte mit Dask so aussehen:
.. literalinclude:: ds_kmeans.py
:caption: ds_kmeans.py
:name: ds_kmeans.py
:lines: 5-
.. toctree::
:hidden:
:titlesonly:
:maxdepth: 0
dask.ipynb
7. Multithreading, Multiprocessing und Async
--------------------------------------------
Nach einem kurzen :doc:`Überblick `
werden anhand von drei Beispielen zu :doc:`Threading `,
:doc:`Multiprocessing ` und :doc:`Async `
die Regeln und Best Practices veranschaulicht.
.. toctree::
:hidden:
:titlesonly:
:maxdepth: 0
multiprocessing-threading-async
threading-example.ipynb
multiprocessing.ipynb
threading-forking-combined.ipynb
asyncio-example.ipynb