Cython zu SciPy hinzufügen#

Wie auf der Cython-Website geschrieben

Cython ist ein optimierender statischer Compiler für die Python-Programmiersprache und die erweiterte Cython-Programmiersprache (basierend auf Pyrex). Es macht das Schreiben von C-Erweiterungen für Python so einfach wie Python selbst.

Wenn Ihr Code derzeit viele Schleifen in Python ausführt, kann eine Kompilierung mit Cython von Vorteil sein. Dieses Dokument ist als sehr kurze Einführung gedacht: gerade genug, um zu sehen, wie man Cython mit SciPy verwendet. Sobald Ihr Code kompiliert, können Sie mehr darüber erfahren, wie Sie ihn optimieren können, indem Sie die Cython-Dokumentation lesen.

Es gibt nur zwei Dinge, die Sie tun müssen, damit SciPy Ihren Code mit Cython kompiliert

  1. Fügen Sie Ihren Code in eine Datei mit der Erweiterung .pyx anstelle einer Erweiterung .py ein. Alle Dateien mit der Erweiterung .pyx werden beim Erstellen von SciPy automatisch von Cython in .c- oder .cpp-Dateien umgewandelt.

  2. Fügen Sie die neue .pyx-Datei zur Build-Konfiguration meson.build des Unterpakets hinzu, in dem Ihr Code liegt. Typischerweise sind bereits andere .pyx-Muster vorhanden (wenn nicht, schauen Sie in einem anderen Untermodul nach), so dass es ein Beispiel gibt, dem Sie folgen können, um den genauen Inhalt für meson.build hinzuzufügen.

Beispiel#

scipy.optimize._linprog_rs.py enthält die Implementierung der überarbeiteten Simplexmethode für scipy.optimize.linprog. Die überarbeitete Simplexmethode führt viele elementare Zeilenoperationen an Matrizen durch und war daher ein natürlicher Kandidat für die Cythonisierung.

Beachten Sie, dass scipy/optimize/_linprog_rs.py die Klassen BGLU und LU aus ._bglu_dense genau so importiert, als wären es reguläre Python-Klassen. Aber das sind sie nicht. BGLU und LU sind Cython-Klassen, die in /scipy/optimize/_bglu_dense.pyx definiert sind. Es gibt nichts an der Art und Weise, wie sie importiert oder verwendet werden, das darauf hindeutet, dass sie in Cython geschrieben sind; die einzige Möglichkeit, wie wir bisher wissen, dass sie Cython-Klassen sind, ist, dass sie in einer Datei mit der Erweiterung .pyx definiert sind.

Selbst in /scipy/optimize/_bglu_dense.pyx ähnelt der Großteil des Codes Python. Die bemerkenswertesten Unterschiede sind das Vorhandensein von cimport, cdef und Cython-Dekoratoren. Keine davon ist streng notwendig. Ohne sie kann der reine Python-Code immer noch von Cython kompiliert werden. Die Cython-Spracherweiterungen sind *nur* Anpassungen zur Leistungsverbesserung. Diese .pyx-Datei wird beim Erstellen von SciPy automatisch von Cython in eine .c-Datei umgewandelt.

Das Einzige, was noch zu tun ist, ist die Hinzufügung der Build-Konfiguration, die in etwa so aussehen wird

_bglu_dense_c = opt_gen.process('_bglu_dense.pyx')

py3.extension_module('_bglu_dense',
  _bglu_dense_c,
  c_args: cython_c_args,
  dependencies: np_dep,
  link_args: version_link_args,
  install: true,
  subdir: 'scipy/optimize'
)

Wenn SciPy erstellt wird, wird _bglu_dense.pyx von cython in C-Code transpilert, und diese generierte C-Datei wird von Meson wie jeder andere C-Code in SciPy behandelt – es entsteht ein Erweiterungsmodul, aus dem wir die Klassen LU und BGLU importieren und verwenden können.

Übung#

Sehen Sie sich ein Video mit einer Durchlaufübung zu diesem Thema an: Cythonizing SciPy Code

  1. Aktualisieren Sie Cython und erstellen Sie einen neuen Branch (z. B. git checkout -b cython_test), in dem Sie einige experimentelle Änderungen an SciPy vornehmen können

  2. Fügen Sie einfachen Python-Code in einer .py-Datei im Verzeichnis /scipy/optimize hinzu, z. B. /scipy/optimize/mypython.py. Zum Beispiel

    def myfun():
        i = 1
        while i < 10000000:
            i += 1
        return i
    
  3. Sehen wir uns an, wie lange diese reine Python-Schleife dauert, um die Leistung von Cython vergleichen zu können. Zum Beispiel in einer IPython-Konsole in Spyder

    from scipy.optimize.mypython import myfun
    %timeit myfun()
    

    Ich erhalte etwas wie

    715 ms ± 10.7 ms per loop
    
  4. Speichern Sie Ihre .py-Datei als .pyx-Datei, z. B. mycython.pyx.

  5. Fügen Sie die .pyx-Datei zu scipy/optimize/meson.build hinzu, wie im vorherigen Abschnitt beschrieben.

  6. Bauen Sie SciPy neu. Beachten Sie, dass ein Erweiterungsmodul (eine .so- oder .pyd-Datei) zum Verzeichnis build/scipy/optimize/ hinzugefügt wurde.

  7. Messen Sie die Zeit, z. B. indem Sie mit python dev.py ipython in IPython wechseln und dann

    from scipy.optimize.mycython import myfun
    %timeit myfun()
    

    Ich erhalte etwas wie

    359 ms ± 6.98 ms per loop
    

    Cython hat den reinen Python-Code um den Faktor ~2 beschleunigt.

  8. Das ist keine große Verbesserung im großen Ganzen. Um zu verstehen, warum, ist es hilfreich, Cython eine „annotierte“ Version unseres Codes erstellen zu lassen, um Engpässe aufzuzeigen. Rufen Sie Cython in einem Terminalfenster mit Ihrer .pyx-Datei mit dem Flag -a auf

    cython -a scipy/optimize/mycython.pyx
    

    Beachten Sie, dass dadurch eine neue .html-Datei im Verzeichnis /scipy/optimize erstellt wird. Öffnen Sie die .html-Datei in einem beliebigen Browser.

  9. Die gelb hervorgehobenen Zeilen in der Datei deuten auf eine potenzielle Interaktion zwischen dem kompilierten Code und Python hin, was die Ausführung erheblich verlangsamt. Die Intensität der Hervorhebung gibt die geschätzte Schwere der Interaktion an. In diesem Fall kann ein Großteil der Interaktion vermieden werden, wenn wir die Variable i als Integer definieren, damit Cython nicht die Möglichkeit berücksichtigen muss, dass es sich um ein allgemeines Python-Objekt handelt.

    def myfun():
        cdef int i = 1  # our first line of Cython code
        while i < 10000000:
            i += 1
        return i
    

    Das Neuerstellen der annotierten .html-Datei zeigt, dass der Großteil der Python-Interaktion verschwunden ist.

  10. Bauen Sie SciPy neu, öffnen Sie eine neue IPython-Konsole und führen Sie %timeit aus

from scipy.optimize.mycython import myfun
%timeit myfun()

Ich erhalte etwas wie: 68.6 ns ± 1.95 ns pro Schleife. Der Cython-Code lief etwa 10 Millionen Mal schneller als der ursprüngliche Python-Code.

In diesem Fall hat der Compiler wahrscheinlich die Schleife wegoptimiert und einfach das Endergebnis zurückgegeben. Diese Art von Beschleunigung ist für echten Code nicht typisch, aber diese Übung veranschaulicht sicherlich die Leistungsfähigkeit von Cython, wenn die Alternative viele Low-Level-Operationen in Python sind.