Heim > Artikel > Backend-Entwicklung > 99 % der Menschen wissen es nicht! Python, C, C-Erweiterungen, Cython-Unterschiede im Vergleich!
Nehmen wir die einfache Fibonacci-Folge als Beispiel, um den Unterschied in ihrer Ausführungseffizienz zu testen.
Python-Code:
def fib(n): a, b = 0.0, 1.0 for i in range(n): a, b = a + b, a return a
C Code :
double cfib(int n) { int i; double a=0.0, b=1.0, tmp; for (i=0; i<n; ++i) { tmp = a; a = a + b; b = tmp; } return a; }
Das Obige ist eine in C implementierte Fibonacci-Sequenz. Einige Leute fragen sich vielleicht, warum wir Gleitkommatypen anstelle von Ganzzahlen verwenden? Die Antwort ist, dass der Integer-Typ von C einen Bereich hat, also verwenden wir Double, und Pythons Float entspricht PyFloatObject auf der untersten Ebene, das auch intern von Double gespeichert wird.
C-Erweiterung:
Dann gibt es noch die C-Erweiterung, Hinweis: Das Schreiben von C-Erweiterungen ist im Wesentlichen dasselbe wie das Schreiben von Cython-Erweiterungen, aber das Schreiben von Cython ist definitiv viel einfacher als das Schreiben von C-Erweiterungen.
#include "Python.h" double cfib(int n) { int i; double a=0.0, b=1.0, tmp; for (i=0; i<n; ++i) { tmp = a; a = a + b; b = tmp; } return a; } static PyObject *fib(PyObject *self, PyObject *n) { if (!PyLong_CheckExact(n)) { wchar_t *error = L"函数 fib 需要接收一个整数"; PyErr_SetObject(PyExc_ValueError, PyUnicode_FromWideChar(error, wcslen(error))); return NULL; } double result = cfib(PyLong_AsLong(n)); return PyFloat_FromDouble(result); } static PyMethodDef methods[] = { {"fib", (PyCFunction) fib, METH_O, "这是 fib 函数"}, {NULL, NULL, 0, NULL} }; static PyModuleDef module = { PyModuleDef_HEAD_INIT, "c_extension", "这是模块 c_extension", -1, methods, NULL, NULL, NULL, NULL }; PyMODINIT_FUNC PyInit_c_extension(void) { return PyModule_Create(&module); }
Sie können sehen, dass selbst ein einfaches Fibonacci eine sehr komplizierte Angelegenheit ist, wenn Sie eine C-Erweiterung schreiben.
Cython-Code:
Endgültiger Blick Wie Cython zum Schreiben von Fibonacci verwenden? Wie sollte Ihrer Meinung nach der in Cython geschriebene Code aussehen?
def fib(int n): cdef int i cdef double a = 0.0, b = 1.0 for i in range(n): a, b = a + b, a return a
Wie wäre es, sind Cython-Codes und Python-Codes sehr ähnlich? Obwohl wir die Syntax von Cython noch nicht offiziell kennengelernt haben, sollten Sie erraten können, was der obige Code bedeutet. Wir definieren eine C-Level-Variable mit dem Schlüsselwort cdef und deklarieren ihren Typ.
Cython-Code muss in ein Erweiterungsmodul kompiliert werden, bevor er vom Interpreter erkannt werden kann. Daher muss er zuerst in C-Code übersetzt und dann in eine Erweiterung kompiliert werden Modul. Auch hier gibt es im Wesentlichen keinen Unterschied zwischen dem Schreiben von C-Erweiterungen und dem Schreiben von Cython-Code, der auch in C-Code übersetzt werden muss.
Aber offensichtlich ist das Schreiben von Cython viel einfacher als das Schreiben von C-Erweiterungen. Wenn die Qualität des geschriebenen Cython-Codes hoch ist, ist es auch die Qualität des übersetzten C-Codes hoch und wird während des Übersetzungsprozesses automatisch weitestgehend optimiert. Wenn es sich jedoch um eine handgeschriebene C-Erweiterung handelt, müssen alle Optimierungen manuell vom Entwickler durchgeführt werden, ganz zu schweigen davon, dass das Schreiben von C-Erweiterungen selbst bei komplexen Funktionen Kopfschmerzen bereitet.
Wenn wir den Cython-Code im Vergleich zum reinen Python-Fibonacci betrachten, sehen wir, dass der Unterschied darin zu liegen scheint, dass die Variablentypen i, a und b vorhanden sind Im Voraus angegeben Das ist es, der Schlüssel liegt darin, warum dies den Beschleunigungseffekt erzielen kann (obwohl es noch nicht getestet wurde, wird die Geschwindigkeit definitiv zunehmen, andernfalls besteht keine Notwendigkeit, Cython zu lernen).
Aber der Grund liegt hier, denn alle Variablen in Python sind generische Zeiger PyObject *. Es gibt zwei Mitglieder in PyObject (einer Struktur von C): ob_refcnt: Speichert den Referenzzähler des Objekts, ob_type *: Speichert den Zeiger des Objekts Typ.
Ob es sich um eine Ganzzahl, eine Gleitkommazahl, eine Zeichenfolge, ein Tupel, ein Wörterbuch oder etwas anderes handelt, alle Variablen, die auf sie verweisen, sind ein PyObject *. Beim Betrieb müssen Sie zuerst den Zeiger des entsprechenden Typs über -> ob_type abrufen und dann die Konvertierung durchführen.
Zum Beispiel a und b im Python-Code wissen wir, dass das Ergebnis unabhängig von der Schleifenebene auf eine Gleitkommazahl zeigt, aber die Der Dolmetscher wird diese Schlussfolgerung nicht ziehen. Jede Addition muss erkannt und konvertiert werden. Gehen Sie dann beim Ausführen der Addition zur internen Methode __add__, um die beiden Objekte hinzuzufügen und nach Abschluss der Ausführung ein neues Objekt zu erstellen zu PyObject * und zurück.
Und Python-Objekte weisen Platz auf dem Heap zu, außerdem sind a und b unveränderlich, sodass jede Schleife ein neues Objekt erstellt und das vorherige ersetzt. Die Objekte werden recycelt.
All dies hat dazu geführt, dass die Ausführungseffizienz von Python-Code unmöglich hoch sein kann. Obwohl Python auch einen Speicherpool und einen entsprechenden Caching-Mechanismus bereitstellt, ist dies offensichtlich der Fall immer noch nicht in der Lage, der geringen Effizienz standzuhalten.
Warum Cython beschleunigen kann, darüber werden wir später sprechen.
Was ist also der Effizienzunterschied zwischen ihnen? Verwenden wir zum Vergleich eine Tabelle:
Der Verbesserungsfaktor bezieht sich darauf, wie oft die Effizienz im Vergleich zu reinem Python verbessert wird.
Die zweite Spalte ist fib(0) und misst offensichtlich nicht die Kosten für den Aufruf einer Funktion. Die vorletzte Spalte „Zeitverbrauch des Schleifenkörpers“ bezieht sich auf die Zeit, die für die Ausführung des inneren Schleifenkörpers bei der Ausführung von fib(90) aufgewendet wurde, ohne den Overhead des Funktionsaufrufs selbst.
Insgesamt ist Fibonacci, geschrieben in reiner C-Sprache, zweifellos am schnellsten, aber es gibt viele Dinge, über die es sich lohnt, darüber nachzudenken.
Pure Python
Erwartungsgemäß ist es in allen Aspekten dasjenige mit der schlechtesten Leistung. Nach fib(0) zu urteilen, dauert der Aufruf einer Funktion 590 Nanosekunden, was viel langsamer ist als in C. Der Grund dafür ist, dass Python beim Aufruf einer Funktion einen Stapelrahmen erstellen muss und dieser Stapelrahmen auf dem Heap zugewiesen wird Letztendlich geht es auch um die Zerstörung von Stapelrahmen usw. Was fib(90) betrifft, ist offensichtlich keine Analyse erforderlich.
Pure C
Zu diesem Zeitpunkt gibt es offensichtlich keine Interaktion mit der Python-Laufzeitumgebung, daher ist der Leistungsverbrauch minimal. fib(0) zeigt, dass C eine Funktion mit einem Overhead von nur 2 Nanosekunden aufruft; fib(90) zeigt, dass C beim Ausführen einer Schleife fast 80-mal schneller ist als Python.
C-Erweiterung
Wie oben erwähnt, dient die C-Erweiterung dazu, C zum Schreiben von Erweiterungsmodulen für Python zu verwenden. Wir betrachten den Zeitverbrauch des Schleifenkörpers und stellen fest, dass die C-Erweiterung fast die gleiche ist wie reines C. Der Unterschied besteht darin, dass mehr Zeit für Funktionsaufrufe aufgewendet wird. Der Grund dafür ist, dass wir beim Aufrufen der Funktion des Erweiterungsmoduls zuerst die Python-Daten in C-Daten konvertieren, dann die C-Funktion verwenden müssen, um die Fibonacci-Folge zu berechnen, und dann die C-Daten nach der Berechnung in Python-Daten konvertieren müssen vollendet.
Die C-Erweiterung ist also im Wesentlichen eine C-Sprache, muss jedoch beim Schreiben der von CPython bereitgestellten API-Spezifikation folgen, damit der C-Code in eine pyd-Datei kompiliert und direkt von Python aufgerufen werden kann. Aus der Sicht der Ergebnisse ist es dasselbe wie Cython. Aber auch hier ist das Schreiben von Erweiterungen in C im Wesentlichen das Schreiben von C, und Sie müssen auch mit der zugrunde liegenden Python/C-API vertraut sein, was relativ schwierig ist.
Cython
Wenn Sie nur den Zeitaufwand für den Schleifenkörper betrachten, sind reines C, C-Erweiterung und Cython ungefähr gleich, aber das Schreiben von Cython ist offensichtlich am bequemsten. Wir sagen, dass das, was Cython macht, im Wesentlichen den C-Erweiterungen ähnelt. Beide stellen Erweiterungsmodule für Python bereit. Der Unterschied besteht darin, dass eines darin besteht, C-Code manuell zu schreiben, und das andere darin, Cython-Code zu schreiben und ihn dann automatisch in C-Code zu übersetzen. Daher ist für Cython der Prozess der Konvertierung von Python-Daten in C-Daten, der Durchführung von Berechnungen und der anschließenden Rückkonvertierung von Python-Daten unvermeidlich.
Aber wir sehen, dass Cython beim Aufrufen von Funktionen viel weniger Zeit benötigt als C-Erweiterungen. Der Hauptgrund ist, dass der von Cython generierte C-Code stark optimiert ist. Aber um ehrlich zu sein, müssen wir uns nicht allzu sehr um die Zeit kümmern, die für Funktionsaufrufe benötigt wird. Wir müssen jedoch auf die Zeit achten, die für die Ausführung interner Codeblöcke benötigt wird. Natürlich werden wir später auch darüber sprechen, wie der Overhead des Funktionsaufrufs selbst reduziert werden kann.
Anhand des Zeitverbrauchs des Schleifenkörpers können wir erkennen, dass die for-Schleife von Python wirklich notorisch langsam ist. Was ist also der Grund? Lassen Sie es uns analysieren.
1. Pythons for-Schleifenmechanismus
Wenn Python ein iterierbares Objekt durchläuft, ruft es zuerst die __iter__-Methode innerhalb des iterierbaren Objekts auf, um seinen entsprechenden Iterator zurückzugeben; und ruft dann kontinuierlich die __next__-Methode des Iterators auf iteriert die Werte nacheinander, bis der Iterator eine StopIteration-Ausnahme auslöst, die von der for-Schleife erfasst wird und die Schleife beendet.
Und Iteratoren sind zustandsbehaftet, und der Python-Interpreter muss jederzeit den Iterationsstatus des Iterators aufzeichnen.
2. Arithmetische Operationen in Python
Wir haben dies oben tatsächlich erwähnt. Aufgrund seiner eigenen dynamischen Eigenschaften kann Python keine typbasierte Optimierung durchführen.
Zum Beispiel: a + b im Schleifenkörper, diese a und b können auf Ganzzahlen, Gleitkommazahlen, Zeichenfolgen, Tupel, Listen oder sogar Instanzobjekte der Klasse verweisen, in der wir die magische Methode __add__ implementiert haben. Warte, warte.
Obwohl wir wissen, dass es sich um eine Gleitkommazahl handelt, geht Python von dieser Annahme nicht aus. Welchen Typ hat also jedes Mal, wenn a + b ausgeführt wird? Stellen Sie dann fest, ob intern eine __add__-Methode vorhanden ist. Wenn ja, führen Sie einen Aufruf mit a und b als Parametern durch, um die Objekte hinzuzufügen, auf die a und b zeigen. Nachdem das Ergebnis berechnet wurde, wird sein Zeiger in PyObject * konvertiert und zurückgegeben.
Für C und Cython wird beim Erstellen einer Variablen der Typ im Voraus als „double“ angegeben, nicht als anderer, sodass das kompilierte a + b nur eine einfache Maschinenanweisung ist. Wie kann Python im Vergleich dazu nicht langsam sein?
3. Speicherzuweisung von Python-Objekten
Python-Objekte werden auf dem Heap zugewiesen, da Python-Objekte im Wesentlichen ein Teil des Speichers sind, der im Heap-Bereich für die Struktur durch die malloc-Funktion von C zugewiesen wird. Das Zuweisen und Freigeben von Speicher im Heap-Bereich erfordert viel Geld, aber der Stapel ist viel kleiner und wird vom Betriebssystem verwaltet und automatisch recycelt, was äußerst effizient ist. Die Zuweisung und Freigabe von Speicher erfolgt nur auf dem Stapel erfordert nur einen einzigen Zug.
Aber der Heap verfügt offensichtlich nicht über diese Behandlung, und Python-Objekte werden alle auf dem Heap zugewiesen. Python führt jedoch einen Speicherpoolmechanismus ein, um eine häufige Interaktion mit dem Betriebssystem bis zu einem gewissen Grad zu vermeiden, und führt auch kleine Ganzzahlen ein Objekt Pool, interner String-Mechanismus, Cache-Pool usw.
Wenn es jedoch um die Erstellung und Zerstörung von Objekten (beliebigen Objekten, einschließlich Skalaren) geht, erhöht sich der Overhead des dynamisch zugewiesenen Speichers und des Speichersubsystems von Python. Das Float-Objekt ist unveränderlich, daher wird es bei jeder Schleife erstellt und zerstört, sodass die Effizienz immer noch nicht hoch ist.
Die von Cython zugewiesenen Variablen (wenn der Typ ein Typ in C ist) sind keine Zeiger mehr (Python-Variablen sind alle Zeiger), für das aktuelle a und b werden sie auf dem Stapel zugewiesen Gleitkomma mit doppelter Genauigkeit Nummer. Die Effizienz der Zuweisung auf dem Stapel ist viel höher als die des Heaps, daher eignet sie sich sehr gut für for-Schleifen, sodass die Effizienz viel höher ist als die von Python. Darüber hinaus ist der Stapel nicht nur bei der Zuweisung, sondern auch bei der Adressierung effizienter als der Heap.
Daher ist es nicht verwunderlich, dass C und Cython bei For-Schleifen um Größenordnungen schneller sind als reines Python, da Python bei jeder Iteration viel Arbeit leistet.
Wir sehen, dass im Cython-Code durch das Hinzufügen einiger CDEFs eine so große Leistungsverbesserung erzielt werden kann, was natürlich sehr aufregend ist. Allerdings erfährt nicht jeder Python-Code enorme Leistungsverbesserungen, wenn er in Cython geschrieben wird.
Das Fibonacci-Sequenzbeispiel, das wir hier haben, ist absichtlich, da die darin enthaltenen Daten an die CPU gebunden sind und die Laufzeit für die Verarbeitung einiger Variablen im CPU-Register aufgewendet wird, ohne dass Daten verschoben werden müssen. Wenn diese Funktion Folgendes ausführt:
Wenn es unser Ziel ist, die Leistung von Python-Programmen zu verbessern, hilft uns das Pareto-Prinzip sehr, das heißt: 80 % der Laufzeit des Programms werden durch 20 % des Codes verursacht. Aber ohne sorgfältige Analyse ist es schwierig, diese 20 Prozent des Codes zu finden. Bevor wir Cython zur Leistungsverbesserung einsetzen, ist daher die Analyse der gesamten Geschäftslogik der erste Schritt.
Wenn wir durch Analyse feststellen, dass der Engpass des Programms durch Netzwerk-IO verursacht wird, können wir nicht erwarten, dass Cython wesentliche Leistungsverbesserungen bringt. Bevor Sie Cython verwenden, müssen Sie daher zunächst ermitteln, was den Engpass im Programm verursacht. Obwohl Cython ein leistungsstarkes Tool ist, muss es richtig eingesetzt werden.
Darüber hinaus hat Cython das C-Typ-System in Python eingeführt, sodass wir auf die Einschränkungen von C-Datentypen achten müssen. Wir wissen, dass die Ganzzahlen von Python nicht durch die Länge eingeschränkt sind, die Ganzzahlen von C jedoch eingeschränkt sind, was bedeutet, dass sie Ganzzahlen mit unendlicher Genauigkeit nicht korrekt darstellen können.
Einige Funktionen von Cython können uns jedoch dabei helfen, diese Überläufe abzufangen. Kurz gesagt, das Wichtigste ist: C-Datentypen sind schneller als Python-Datentypen, aber sie unterliegen Einschränkungen. Es ist nicht flexibel und vielseitig genug. Hier können wir auch erkennen, dass sich Python im Hinblick auf Geschwindigkeit, Flexibilität und Vielseitigkeit für Letzteres entschieden hat.
Beachten Sie außerdem eine weitere Funktion von Cython: die Verbindung mit externem Code. Angenommen, unser Ausgangspunkt ist nicht Python, sondern C oder C++, und wir möchten Python verwenden, um mehrere C- oder C++-Module zu verbinden. Cython versteht C- und C++-Deklarationen und kann hochoptimierten Code generieren, sodass es besser als Brücke geeignet ist.
Da ich ein Python-Hauptfach bin, werde ich, wenn es um C und C++ geht, vorstellen, wie man C und C++ in Cython einführt und die bereits geschriebene C-Bibliothek direkt aufruft. Es wird nicht vorgestellt, wie man Cython als Brücke zwischen mehreren C- und C++-Modulen in C und C++ einführt. Ich hoffe, Sie verstehen das, denn ich verwende weder C noch C++ zum Schreiben von Diensten, sondern verwende sie nur, um Python bei der Verbesserung der Effizienz zu unterstützen.
Bisher haben wir nur Cython vorgestellt und hauptsächlich seine Positionierung und die Unterschiede besprochen zwischen Python und C. Die Verwendung von Cython zur Beschleunigung von Python, das Schreiben von Cython-Code und seine detaillierte Syntax werden wir später vorstellen.
Kurz gesagt, Cython ist eine ausgereifte Sprache, die Python bedient. Cython-Code kann nicht direkt ausgeführt werden, da er nicht den Syntaxregeln von Python entspricht.
Die Art und Weise, wie wir Cython verwenden, ist: Zuerst den Cython-Code in C-Code übersetzen, dann den C-Code in ein Erweiterungsmodul (PYD-Datei) kompilieren und dann in Python-Code Es zu importieren und die darin enthaltenen Funktionsmethoden aufzurufen, ist der richtige Weg und natürlich der einzige Weg für uns, Cython zu verwenden.
Zum Beispiel meldet das oben in Cython geschriebene Fibonacci einen Fehler, wenn es direkt ausgeführt wird, da cdef offensichtlich nicht den Syntaxregeln von Python entspricht. Daher muss der Cython-Code in ein Erweiterungsmodul kompiliert und dann in eine normale Py-Datei importiert werden. Dies hat die Bedeutung, die Laufgeschwindigkeit zu verbessern. Daher sollten Cython-Codes CPU-intensive Codes sein, da es sonst schwierig ist, die Effizienz erheblich zu verbessern.
Vor der Verwendung von Cython ist es daher am besten, die Geschäftslogik sorgfältig zu analysieren oder Cython vorerst nicht zu verwenden und es vollständig in Python zu schreiben. Beginnen Sie nach Abschluss des Schreibens mit dem Testen und Analysieren der Leistung des Programms, um festzustellen, wo es mehr Zeit in Anspruch nimmt, es aber gleichzeitig durch statische Typisierung optimiert werden kann. Suchen Sie sie, schreiben Sie sie in Cython neu, kompilieren Sie sie in Erweiterungsmodule und rufen Sie dann die Funktionen im Erweiterungsmodul auf.
Das obige ist der detaillierte Inhalt von99 % der Menschen wissen es nicht! Python, C, C-Erweiterungen, Cython-Unterschiede im Vergleich!. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!