Wie ich von Python zu Julia gewechselt bin (und warum)

Ein kleiner Hintergrund über Python


Python ist eine großartige Sprache. Ich habe vorher mehrere Sprachen ausprobiert: Pascal in der Schule; C, C mit Klassen, C ++ an der Universität. Die letzten zwei (drei) haben eine starke Abneigung gegen die Programmierung ausgelöst: Anstatt das Problem zu lösen, spielen Sie mit Zuordnungen und Destruktoren (beängstigende Wörter aus der Vergangenheit), Sie denken in Grundelementen auf niedriger Ebene. Meiner Meinung nach ist C nicht zur Lösung von pädagogischen und wissenschaftlichen Problemen geeignet (auf jeden Fall im Bereich der Mathematik). Ich bin sicher, dass sie Einwände gegen mich erheben werden, aber ich versuche niemandem etwas aufzuzwingen, ich drücke einfach meine Meinung aus.

Python war einmal eine Offenbarung. Zum ersten Mal in meinem Leben schrieb ich mehrere Abstraktionsebenen, die höher waren als in C üblich. Der Abstand zwischen der Aufgabe und dem Code wurde wie nie zuvor verringert.

Ich hätte dies wahrscheinlich mein ganzes Leben in Python getan, wenn ich nicht plötzlich statistische NIST-Tests implementieren müsste. Es scheint, dass die Aufgabe sehr einfach ist: Es gibt ein Array mit einer Länge von mehreren (> = 10) Megabyte, es gibt eine Reihe von Tests, die auf dieses Array angewendet werden müssen.

Wofür ist gut numpy?


In Python gibt es ein gutes Numpy-Paket für die Arbeit mit Arrays, das im Wesentlichen eine übergeordnete Schnittstelle für schnelle Bibliotheken wie LAPACK darstellt. Es scheint, dass das gesamte Gentleman-Set für wissenschaftliches Rechnen verfügbar ist (Sympy, Numpy, Scipy, Matplotlib). Was möchten Sie mehr?

Jeder, der sich mit Numpy befasst hat, weiß, dass er gut ist, aber nicht in allem. Wir müssen auch versuchen sicherzustellen, dass die Operationen vektorisiert sind (wie in R), d.h. in gewissem Sinne einheitlich für das gesamte Array. Wenn Ihr Problem aus irgendeinem Grund plötzlich nicht mehr in dieses Paradigma passt, haben Sie Probleme.

Über welche nicht vektorisierten Aufgaben spreche ich? Ja, nehmen Sie mindestens das gleiche NIST: Berechnen Sie die Länge des linearen Schieberegisters mit dem Berlekamp-Messi-Algorithmus. Berechnen Sie die Länge der längsten Teilsequenz aufeinanderfolgender Einheiten usw. Ich schließe die Möglichkeit nicht aus, dass es eine geniale Lösung gibt, die das Problem auf eine vektorisierte reduziert.

List?
Als Beispiel aus demselben NIST: Es war notwendig, die Anzahl der "Schalt" -Sequenzen zu berechnen, wobei mit "Umschalten" das Ändern aufeinanderfolgender Einheiten (... 1111 ...) in aufeinanderfolgende Nullen (... 000 ...) gemeint ist. ), umgekehrt. Dazu können Sie die ursprüngliche Sequenz ohne das letzte Element (x [: -1]) nehmen und die um 1 verschobene Sequenz (x [2:]) davon subtrahieren und dann die Anzahl der Elemente ungleich Null in der resultierenden neuen Sequenz berechnen. Alles zusammen wird aussehen wie:

np.count_nonzero(x[:-1] - x[1:]) 

Es mag wie ein unterhaltsames Training für den Geist aussehen, aber im Wesentlichen passiert hier etwas Unnatürliches, ein Trick, der dem Leser nach kurzer Zeit nicht klar sein wird. Ganz zu schweigen davon, dass dies immer noch langsam ist - niemand hat die Speicherzuordnung abgebrochen.

Nicht vektorisierte Operationen in Python sind eine lange Zeit. Wie gehe ich mit ihnen um, wenn Numpy nicht genug ist? Normalerweise bieten sie mehrere Lösungen an:

  1. Numba JIT. Wenn sie wie auf der Numba-Titelseite beschrieben arbeiten würde, würde es sich meiner Meinung nach lohnen, die Geschichte zu beenden. Ich hatte in der Vergangenheit etwas vergessen, was mit ihr schief gelaufen war; Das Fazit ist, dass die Beschleunigung leider nicht so beeindruckend war, wie ich erwartet hatte.
  2. Cython. OK, heben Sie Ihre Hände diejenigen, die glauben, dass Cython eine wirklich schöne, elegante Lösung ist, die nicht die Bedeutung und den Geist von Python zerstört? Ich denke so nicht; Wenn Sie nach Cython kommen, können Sie bereits aufhören, sich selbst zu täuschen, und zu etwas weniger Anspruchsvollem wie C ++ und C wechseln.
  3. Schreiben Sie die Engpässe in C neu und ziehen Sie sie aus Ihrem geliebten Python heraus. OK, aber was ist, wenn ich das gesamte Programm habe - es geht nur um Berechnungen und Engpässe? Xi bietet nicht! Ich spreche nicht von der Tatsache, dass Sie in dieser Lösung nicht nur eine, sondern zwei Sprachen beherrschen müssen - Python und C.

Hier kommt die JULIA!


Nachdem ich über vorhandene Lösungen nachgedacht hatte und nichts Gutes gefunden hatte (nicht programmieren konnte), beschloss ich, es mit etwas „Schnellerem“ umzuschreiben. In der Tat, wenn Sie im 21. Jahrhundert mit normaler Unterstützung für Arrays, vektorisierte Operationen "out of the box" usw. "Dreschmaschine für Zahlen" schreiben. usw., dann ist Ihre Wahl nicht sehr dicht:

  1. Fortran . Und nicht lachen, wer von uns hat BLAS / LAPACK nicht aus unseren Lieblingssprachen gezogen? Fortran ist eine wirklich gute (bessere SI!) Sprache für SCIENTIFIC Computing. Ich habe es nicht aus dem Grund genommen, dass seit der Zeit von Fortran viele Dinge erfunden und zu Programmiersprachen hinzugefügt wurden; Ich hatte auf etwas Moderneres gehofft.
  2. R leidet unter den gleichen Problemen wie Python (Vektorisierung).
  3. Matlab - wahrscheinlich ja, ich habe leider kein Geld zum Überprüfen.
  4. Julia - das Pferd ist dunkel, wird abheben, wird nicht abheben (und es war natürlich bis zur stabilen Version 1.0)

Einige offensichtliche Vorteile von Julia


  1. Es sieht aus wie Python, zumindest das gleiche "High-Level", mit der Fähigkeit, bei Bedarf in Zahlen zu zerfallen.
  2. Keine Aufregung mit Speicherzuordnungen und dergleichen.
  3. Leistungsstarkes Typensystem. Typen werden optional und sehr dosiert verschrieben. Ein Programm kann ohne Angabe eines einzelnen Typs geschrieben werden - und wenn Sie dies tun, können sie es schnell tun. Aber es gibt Nuancen.
  4. Es ist einfach, benutzerdefinierte Typen zu schreiben, die genauso schnell sind wie die integrierten Typen.
  5. Mehrfachversand. Sie können stundenlang darüber sprechen, aber meiner Meinung nach - dies ist das Beste, was Julia hat, es ist die Grundlage für die Gestaltung aller Programme und im Allgemeinen die Grundlage für die Philosophie der Sprache.
    Dank des Mehrfachversands sind viele Dinge sehr einheitlich geschrieben.

    Mehrere Versandbeispiele
     rand() #       0  1 rand(10) #   10     0  1 rand(3,5) #   3  5   .... using Distributions d = Normal() #     0, 1 rand(d) #     rand(d, 10) #   10 ...    

    Das heißt, das erste Argument kann eine beliebige (eindimensionale) Verteilung von Verteilungen sein, aber der Funktionsaufruf bleibt buchstäblich gleich. Und nicht nur die Verteilung (es ist möglich, das RNG selbst als MersenneTwister-Objekt zu übertragen und vieles mehr). Ein weiteres (meiner Meinung nach veranschaulichendes) Beispiel ist die Navigation in DataFrames ohne loc / iloc.
  6. 6. Arrays sind native, integrierte Arrays. Vektorisierung aus der Box.

Nachteile zu schweigen, was ein Verbrechen wäre!


  1. Neue Sprache. Einerseits natürlich ein Minus. In etwas vielleicht unreifem. Auf der anderen Seite berücksichtigte er den Rechen vieler früherer Sprachen, steht „auf den Schultern von Riesen“, hat viele interessante und neue Dinge aufgenommen.
  2. Es ist unwahrscheinlich, dass sofort schnelle Programme schreiben. Es gibt einige nicht offensichtliche Dinge, die sehr einfach zu handhaben sind: Sie treten auf den Rechen, bitten die Community um Hilfe, treten erneut ... usw. Dies sind hauptsächlich Typinstabilität, Typinstabilität und globale Variablen. Soweit ich selbst sagen kann, durchläuft ein Programmierer, der schnell in Julia schreiben möchte, im Allgemeinen mehrere Phasen: a) schreibt in Python. Das ist großartig und so ist es auch, aber manchmal gibt es Leistungsprobleme. b) schreibt wie in C: wo immer möglich Typen manuell vorschreiben. c) versteht allmählich, wo es notwendig (sehr gemessen) ist, Typen zu verschreiben, und wo sie sich gegenseitig stören.
  3. Ökosystem Einige Pakete sind roh in dem Sinne, dass irgendwo ständig etwas abfällt; Einige sind ausgereift, aber nicht miteinander vereinbar (eine Umstellung auf andere Typen ist beispielsweise erforderlich). Einerseits ist das offensichtlich schlecht; Auf der anderen Seite hat Julia viele interessante und mutige Ideen, nur weil „wir auf den Schultern der Riesen stehen“ - wir haben enorme Erfahrungen mit dem Betreten eines Rechen und „wie man es nicht macht“ gesammelt, und dies wird (teilweise) von den Paketentwicklern berücksichtigt.
  4. Nummerierung von Arrays ab 1. Ha, Scherz, das ist natürlich ein Plus! Nein, im Ernst, was ist los, mit welchem ​​Index beginnt die Nummerierung? Man gewöhnt sich in 5 Minuten daran. Niemand beschwert sich darüber, dass Ganzzahlen Ganzzahlen genannt werden, nicht ganze. Das sind alles Geschmackssachen. Und ja, nehmen Sie mindestens das gleiche Cormen gemäß den Algorithmen - alles ist dort von einem nummeriert, und es ist manchmal unpraktisch, es im Gegenteil in Python zu wiederholen.

Warum Geschwindigkeit?


Es ist Zeit, sich daran zu erinnern, warum alles begonnen wurde.

Hinweis: Ich habe Python sicher vergessen. Schreiben Sie Ihre Verbesserungen in die Kommentare. Ich werde versuchen, sie auf meinem Laptop auszuführen und die Laufzeit anzuzeigen.

Also zwei kleine Beispiele mit Mikrobenchmarks.

Etwas Vektorisiertes
Problem: Ein Vektor X von 0 und 1 wird dem Eingang der Funktion zugeführt. Es ist notwendig, ihn in einen Vektor von 1 und (-1) (1 -> 1, 0 -> -1) umzuwandeln und zu berechnen, wie viele Koeffizienten aus der Fourier-Transformation dieses Vektors sind Absolutwert überschreitet die Grenzgrenze.

 def process_fft(x, boundary): return np.count_nonzero(np.abs(np.fft.fft(2*x-1)) > boundary) %timeit process_fft(x, 2000) 84.1 ms ± 335 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) 

 function process_fft(x, boundary) count(t -> t > boundary, abs.(fft(2*x.-1))) end @benchmark process_fft(x, 2000) BenchmarkTools.Trial: memory estimate: 106.81 MiB allocs estimate: 61 -------------- minimum time: 80.233 ms (4.75% GC) median time: 80.746 ms (4.70% GC) mean time: 85.000 ms (8.67% GC) maximum time: 205.831 ms (52.27% GC) -------------- samples: 59 evals/sample: 1 

Wir werden hier nichts Überraschendes sehen - trotzdem betrachten sie es nicht selbst, sondern geben es an gut optimierte fortran-Programme weiter.

Etwas nicht Vektorisierbares
Dem Eingang wird ein Array von 0 und 1 zugeführt. Ermitteln Sie die Länge der längsten Teilsequenz aufeinanderfolgender Einheiten.

 def longest(x): maxLen = 0 currLen = 0 # This will count the number of ones in the block for bit in x: if bit == 1: currLen += 1 maxLen = max(maxLen, currLen) else: maxLen = max(maxLen, currLen) currLen = 0 return max(maxLen, currLen) %timeit longest(x) 599 ms ± 639 µs per loop (mean ± std. dev. of 7 runs, 1 loop each) 

 function longest(x) maxLen = 0 currLen = 0 # This will count the number of ones in the block for bit in x if bit == 1 currLen += 1 maxLen = max(maxLen, currLen) else maxLen = max(maxLen, currLen) currLen = 0 end end return max(maxLen, currLen) end @benchmark longest(x) BenchmarkTools.Trial: memory estimate: 0 bytes allocs estimate: 0 -------------- minimum time: 9.094 ms (0.00% GC) median time: 9.248 ms (0.00% GC) mean time: 9.319 ms (0.00% GC) maximum time: 12.177 ms (0.00% GC) -------------- samples: 536 evals/sample: 1 

Der Unterschied ist mit bloßem Auge offensichtlich. Tipps zum "Fertigstellen" und / oder Vektorisieren der Numpy-Version sind willkommen. Ich möchte auch darauf hinweisen, dass die Programme fast identisch sind. Zum Beispiel habe ich in Julia keinen einzigen Typ registriert (vergleiche mit dem vorherigen) - trotzdem funktioniert alles schnell.

Ich möchte auch darauf hinweisen, dass die vorgestellten Versionen nicht im endgültigen Programm enthalten waren, sondern weiter optimiert wurden. hier werden sie als Beispiel und ohne unnötige Komplikationen angegeben (Weiterleiten von zusätzlichem Speicher in Julia, um rfft an Ort und Stelle auszuführen usw.).

Was kam am Ende heraus?


Ich werde den endgültigen Code für Python und für Julia nicht zeigen: Dies ist ein Geheimnis (zumindest für den Moment). Als ich mit dem Beenden der Python-Version aufhörte, wurden alle NIST-Tests auf einem Array von 16 Megabyte Binärzeichen in ~ 50 Sekunden durchgeführt. Bei Julia laufen derzeit alle Tests auf dem gleichen Volumen ~ 20 Sekunden. Es kann gut sein, dass ich ein Dummkopf bin und es Raum für Optimierungen usw. gab. usw. Aber das bin ich, wie ich bin, mit all seinen Vor- und Nachteilen, und meiner Meinung nach sollte nicht die sphärische Geschwindigkeit von Programmen in einem Vakuum in Programmiersprachen berücksichtigt werden, sondern wie ich sie persönlich programmieren kann (ohne wirklich grobe Fehler).

Warum habe ich das alles geschrieben?


Die Leute hier wurden interessiert; Ich beschloss zu schreiben, wenn die Zeit gekommen ist. Ich denke, ich werde Julia in Zukunft genauer durchgehen und ein Beispiel für die Lösung einiger typischer Probleme geben. Warum? Mehr Sprachen, gut und anders, und lassen Sie jeden, der etwas finden möchte, das zu ihm persönlich passt.

Source: https://habr.com/ru/post/de427879/


All Articles