Gibt es technische Einschränkungen oder Sprachfunktionen, die verhindern, dass mein Python-Skript so schnell ist wie ein gleichwertiges C ++ - Programm?

10

Ich bin ein langjähriger Python-Benutzer. Vor ein paar Jahren habe ich angefangen, C ++ zu lernen, um zu sehen, was es in Bezug auf Geschwindigkeit bieten kann. Während dieser Zeit würde ich Python weiterhin als Werkzeug für das Prototyping verwenden. Dies schien ein gutes System zu sein: agile Entwicklung mit Python, schnelle Ausführung in C ++.

In letzter Zeit habe ich Python immer mehr verwendet und gelernt, wie man all die Fallstricke und Anti-Patterns vermeidet, die ich in meinen früheren Jahren mit der Sprache schnell verwendet habe. Nach meinem Verständnis kann die Verwendung bestimmter Funktionen (Listenverständnis, Aufzählungen usw.) die Leistung steigern.

Aber gibt es technische Einschränkungen oder Sprachfunktionen, die verhindern, dass mein Python-Skript so schnell ist wie ein gleichwertiges C ++ - Programm?

KidElephant
quelle
2
Ja, kann es. Informationen zum Stand der Technik in Python-Compilern finden Sie unter PyPy .
Greg Hewgill
5
Alle Variablen in Python sind polymorph, dh der Typ der Variablen ist nur zur Laufzeit bekannt. Wenn Sie (unter der Annahme von ganzen Zahlen) x + y in C-ähnlichen Sprachen sehen, fügen sie eine ganze Zahl hinzu. In Python werden die Variablentypen auf x und y umgeschaltet, und dann wird die entsprechende Additionsfunktion ausgewählt, und dann wird eine Überlaufprüfung durchgeführt, und dann wird die Addition durchgeführt. Wenn Python nicht das statische Tippen lernt, wird dieser Overhead niemals verschwinden.
nwp
1
@nwp Nein, das ist einfach, siehe PyPy. Zu den schwierigeren, noch offenen Problemen gehören: Wie man die Startlatenz von JIT-Compilern überwindet, wie man Zuordnungen für komplizierte langlebige Objektgraphen vermeidet und wie man den Cache im Allgemeinen gut nutzt.

Antworten:

11

Ich habe diese Wand selbst getroffen, als ich vor ein paar Jahren einen Vollzeit-Python-Programmierjob annahm. Ich liebe Python, das tue ich wirklich, aber als ich anfing, die Leistung zu optimieren, hatte ich einige unhöfliche Schocks.

Die strengen Pythonisten können mich korrigieren, aber hier sind die Dinge, die ich gefunden habe, gemalt in sehr breiten Strichen.

  • Die Verwendung des Python-Speichers ist beängstigend. Python repräsentiert alles als Diktat - was extrem mächtig ist, aber dazu führt, dass selbst einfache Datentypen gigantisch sind. Ich erinnere mich, dass das Zeichen "a" 28 Byte Speicherplatz beanspruchte. Wenn Sie Big-Data-Strukturen in Python verwenden, sollten Sie sich auf numpy oder scipy verlassen, da diese durch eine direkte Byte-Array-Implementierung unterstützt werden.

Dies wirkt sich auf die Leistung aus, da zur Laufzeit zusätzliche Indirektionsebenen vorhanden sind und im Vergleich zu anderen Sprachen nicht viel Speicherplatz benötigt wird.

  • Python verfügt über eine globale Interpretersperre, was bedeutet, dass Prozesse zum größten Teil mit einem Thread ausgeführt werden. Möglicherweise gibt es Bibliotheken, die Aufgaben auf mehrere Prozesse verteilen, aber wir haben ungefähr 32 Instanzen unseres Python-Skripts gestartet und jeden einzelnen Thread ausgeführt.

Andere können mit dem Ausführungsmodell sprechen, aber Python wird zur Laufzeit kompiliert und dann interpretiert, was bedeutet, dass es nicht bis zum Maschinencode reicht. Das wirkt sich auch auf die Leistung aus. Sie können problemlos C- oder C ++ - Module verknüpfen oder finden, aber wenn Sie Python direkt ausführen, wird dies einen Leistungseinbruch bedeuten.

In Webdienst-Benchmarks ist Python jetzt im Vergleich zu anderen zur Laufzeit kompilierbaren Sprachen wie Ruby oder PHP günstig. Aber es ist ziemlich weit hinter den meisten kompilierten Sprachen zurück. Sogar die Sprachen, die zu einer Zwischensprache kompiliert und in einer VM ausgeführt werden (wie Java oder C #), sind viel, viel besser.

Hier ist eine wirklich interessante Reihe von Benchmark-Tests, auf die ich gelegentlich Bezug nehme:

http://www.techempower.com/benchmarks/

(Trotzdem liebe ich Python immer noch sehr und wenn ich die Möglichkeit habe, die Sprache zu wählen, in der ich arbeite, ist dies meine erste Wahl. Meistens bin ich sowieso nicht an verrückte Durchsatzanforderungen gebunden.)

rauben
quelle
2
Die Zeichenfolge "a" ist kein gutes Beispiel für den ersten Aufzählungspunkt. Eine Java-Zeichenfolge hat auch einen erheblichen Overhead für einzelne Zeichenfolgen, aber es ist ein konstanter Overhead, der sich mit zunehmender Länge der Zeichenfolge recht gut amortisiert (ein bis vier Byte mehr Zeichen je nach Version, Erstellungsoptionen und Inhalt der Zeichenfolge). Sie haben jedoch Recht mit benutzerdefinierten Objekten, zumindest solchen, die nicht verwendet werden __slots__. PyPy sollte in dieser Hinsicht viel besser abschneiden, aber ich weiß nicht genug, um es beurteilen zu können.
1
Das zweite Problem, auf das Sie hinweisen, bezieht sich nur auf eine bestimmte Implementierung und nicht auf die Sprache. Das erste Problem muss erklärt werden: Was 28 Bytes "wiegt", ist nicht das Zeichen selbst, sondern die Tatsache, dass es in eine Zeichenfolgenklasse gepackt wurde und über eigene Methoden und Eigenschaften verfügt. Die Darstellung eines einzelnen Zeichens als Byte-Array (Literal b'a ') "nur" wiegt in Python 3.3 18 Byte, und ich bin sicher, dass es weitere Möglichkeiten gibt, die Zeichenspeicherung im Speicher zu optimieren, wenn Ihre Anwendung dies wirklich benötigt.
Red
C # kann nativ kompiliert werden (z. B. kommende MS-Technologie, Xamarin für iOS).
Den
13

Die Python-Referenzimplementierung ist der CPython-Interpreter. Es versucht relativ schnell zu sein, verwendet jedoch derzeit keine erweiterten Optimierungen. Und für viele Verwendungsszenarien ist dies eine gute Sache: Die Kompilierung zu einem Zwischencode erfolgt unmittelbar vor der Laufzeit, und jedes Mal, wenn das Programm ausgeführt wird, wird der Code neu kompiliert. Die für die Optimierung benötigte Zeit muss also gegen die durch Optimierungen gewonnene Zeit abgewogen werden. Wenn kein Nettogewinn erzielt wird, ist die Optimierung wertlos. Für ein sehr lang laufendes Programm oder ein Programm mit sehr engen Schleifen wäre die Verwendung erweiterter Optimierungen nützlich. CPython wird jedoch für einige Jobs verwendet, die eine aggressive Optimierung ausschließen:

  • Kurz laufende Skripte, die zB für Sysadmin-Aufgaben verwendet werden. Viele Betriebssysteme wie Ubuntu bauen einen Großteil ihrer Infrastruktur auf Python auf: CPython ist schnell genug für den Job, hat jedoch praktisch keine Startzeit. Solange es schneller als Bash ist, ist es gut.

  • CPython muss eine klare Semantik haben, da es sich um eine Referenzimplementierung handelt. Dies ermöglicht einfache Optimierungen wie "Optimieren der Implementierung des foo-Operators" oder "Kompilieren von Listenverständnissen zu schnellerem Bytecode", schließt jedoch im Allgemeinen Optimierungen aus, die Informationen zerstören, wie z. B. Inlining-Funktionen.

Natürlich gibt es mehr Python-Implementierungen als nur CPython:

  • Jython basiert auf der JVM. Die JVM kann den bereitgestellten Bytecode interpretieren oder JIT-kompilieren und verfügt über profilgesteuerte Optimierungen. Es leidet unter einer hohen Startzeit und es dauert eine Weile, bis die JIT einsetzt.

  • PyPy ist ein Stand der Technik, JITting Python VM. PyPy ist in RPython geschrieben, einer eingeschränkten Teilmenge von Python. Diese Teilmenge entfernt etwas Ausdruckskraft aus Python, ermöglicht jedoch die statische Ableitung des Typs einer beliebigen Variablen. Die in RPython geschriebene VM kann dann in C transpiliert werden, was eine RPython C-ähnliche Leistung ergibt. RPython ist jedoch immer noch ausdrucksstärker als C, was eine schnellere Entwicklung neuer Optimierungen ermöglicht. PyPy ist ein Beispiel für Compiler-Bootstrapping. PyPy (nicht RPython!) Ist größtenteils mit der CPython-Referenzimplementierung kompatibel.

  • Cython ist (wie RPython) ein inkompatibler Python-Dialekt mit statischer Typisierung. Es wird auch in C-Code transpiliert und kann problemlos C-Erweiterungen für den CPython-Interpreter generieren.

Wenn Sie bereit sind, Ihren Python-Code in Cython oder RPython zu übersetzen, erhalten Sie eine C-ähnliche Leistung. Sie sollten jedoch nicht als "Teilmenge von Python" verstanden werden, sondern als "C mit pythonischer Syntax". Wenn Sie zu PyPy wechseln, wird Ihr Vanille-Python-Code erheblich beschleunigt, kann aber auch nicht mit in C oder C ++ geschriebenen Erweiterungen verbunden werden.

Aber welche Eigenschaften oder Merkmale verhindern, dass Vanilla Python abgesehen von langen Startzeiten ein C-ähnliches Leistungsniveau erreicht?

  • Mitwirkende und Finanzierung. Im Gegensatz zu Java oder C # gibt es keine einzige Fahrgesellschaft hinter der Sprache, die daran interessiert ist, diese Sprache zur besten ihrer Klasse zu machen. Dies beschränkt die Entwicklung hauptsächlich auf Freiwillige und gelegentliche Zuschüsse.

  • Späte Bindung und das Fehlen jeglicher statischer Typisierung. Python erlaubt es uns, Mist wie folgt zu schreiben:

    import random
    
    # foo is a function that returns an empty list
    def foo(): return []
    
    # foo is a function, right?
    # this ought to be equivalent to "bar = foo"
    def bar(): return foo()
    
    # ooh, we can reassign variables to a different type – randomly
    if random.randint(0, 1):
       foo = 42
    
    print bar()
    # why does this blow up (in 50% of cases)?
    # "foo" was a function while "bar" was defined!
    # ah, the joys of late binding

    In Python kann jede Variable jederzeit neu zugewiesen werden. Dies verhindert das Zwischenspeichern oder Inlining. Jeder Zugriff muss über die Variable erfolgen. Diese Indirektion belastet die Leistung. Natürlich: Wenn Ihr Code solche verrückten Dinge nicht tut, so dass jeder Variablen vor dem Kompilieren ein definitiver Typ zugewiesen werden kann und jede Variable nur einmal zugewiesen wird, könnte theoretisch ein effizienteres Ausführungsmodell ausgewählt werden. Eine Sprache in diesem Sinne würde eine Möglichkeit bieten, Bezeichner als Konstanten zu markieren und zumindest optionale Typanmerkungen zuzulassen („schrittweise Eingabe“).

  • Ein fragwürdiges Objektmodell. Wenn keine Slots verwendet werden, ist es schwierig herauszufinden, welche Felder ein Objekt hat (ein Python-Objekt ist im Wesentlichen eine Hash-Tabelle von Feldern). Und selbst wenn wir dort sind, haben wir noch keine Ahnung, welche Typen diese Felder haben. Dies verhindert, dass Objekte als dicht gepackte Strukturen dargestellt werden, wie dies in C ++ der Fall ist. (Natürlich ist die Darstellung von Objekten in C ++ auch nicht ideal: Aufgrund der strukturellen Natur gehören sogar private Felder zur öffentlichen Schnittstelle eines Objekts.)

  • Müllabfuhr. In vielen Fällen konnte die GC vollständig vermieden werden. Mit C ++ können wir Objekte statisch zuordnen, die automatisch zerstört werden, wenn der aktuelle Bereich verlassen wird : Type instance(args);. Bis dahin lebt das Objekt und kann an andere Funktionen verliehen werden. Dies erfolgt normalerweise über „Pass-by-Reference“. Mit Sprachen wie Rust kann der Compiler statisch überprüfen, ob kein Zeiger auf ein solches Objekt die Lebensdauer des Objekts überschreitet. Dieses Speicherverwaltungsschema ist vollständig vorhersehbar, hocheffizient und eignet sich für die meisten Fälle ohne komplizierte Objektgraphen. Leider wurde Python nicht für die Speicherverwaltung entwickelt. Theoretisch kann die Fluchtanalyse verwendet werden, um Fälle zu finden, in denen GC vermieden werden kann. In der Praxis können einfache Methodenketten wiefoo().bar().baz() muss eine große Anzahl kurzlebiger Objekte auf dem Heap zuordnen (Generations-GC ist eine Möglichkeit, dieses Problem klein zu halten).

    In anderen Fällen kennt der Programmierer möglicherweise bereits die endgültige Größe eines Objekts, z. B. einer Liste. Leider bietet Python keine Möglichkeit, dies beim Erstellen einer neuen Liste zu kommunizieren. Stattdessen werden neue Elemente an das Ende verschoben, was möglicherweise mehrere Neuzuweisungen erfordert. Ein paar Anmerkungen:

    • Listen einer bestimmten Größe können wie erstellt werden fixed_size = [None] * size. Der Speicher für die Objekte in dieser Liste muss jedoch separat zugewiesen werden. Kontrast C ++, wo wir tun können std::array<Type, size> fixed_size.

    • Gepackte Arrays eines bestimmten nativen Typs können in Python über das arrayintegrierte Modul erstellt werden. Bietet außerdem numpyeine effiziente Darstellung von Datenpuffern mit bestimmten Formen für native numerische Typen.

Zusammenfassung

Python wurde für eine einfache Bedienung entwickelt, nicht für die Leistung. Das Design erschwert die Erstellung einer hocheffizienten Implementierung. Wenn der Programmierer auf problematische Funktionen verzichtet, kann ein Compiler, der die verbleibenden Redewendungen versteht, effizienten Code ausgeben, der in seiner Leistung mit C mithalten kann.

amon
quelle
8

Ja. Das Hauptproblem besteht darin, dass die Sprache als dynamisch definiert ist - das heißt, Sie wissen nie, was Sie tun, bis Sie es tun. Das macht es sehr schwer , eine effiziente Maschinencode zu erzeugen, weil Sie nicht wissen , was zu produzieren Maschinencode für . JIT-Compiler können in diesem Bereich einige Arbeiten ausführen, sind jedoch nie mit C ++ vergleichbar, da der JIT-Compiler einfach keine Zeit und keinen Speicher für die Ausführung aufwenden kann, da dies Zeit und Speicher ist, die Sie nicht für die Ausführung Ihres Programms aufwenden, und es gibt strenge Grenzen für die Ausführung Sie können erreichen, ohne die dynamische Sprachsemantik zu brechen.

Ich werde nicht behaupten, dass dies ein inakzeptabler Kompromiss ist. Für die Natur von Python ist es jedoch von grundlegender Bedeutung, dass echte Implementierungen niemals so schnell sind wie C ++ - Implementierungen.

DeadMG
quelle
8

Es gibt drei Hauptfaktoren, die die Leistung aller dynamischen Sprachen beeinflussen, einige mehr als andere.

  1. Interpretationsaufwand. Zur Laufzeit gibt es eher eine Art Bytecode als Maschinenanweisungen, und die Ausführung dieses Codes ist mit einem festen Aufwand verbunden.
  2. Versandkosten. Das Ziel für einen Funktionsaufruf ist erst zur Laufzeit bekannt, und das Herausfinden, welche Methode aufgerufen werden soll, ist mit Kosten verbunden.
  3. Speicherverwaltungsaufwand. Dynamische Sprachen speichern Inhalte in Objekten, die zugewiesen und freigegeben werden müssen und die einen Leistungsaufwand verursachen.

Für C / C ++ sind die relativen Kosten dieser 3 Faktoren nahezu Null. Anweisungen werden direkt vom Prozessor ausgeführt, der Versand dauert höchstens ein oder zwei Indirektionen, der Heap-Speicher wird niemals zugewiesen, es sei denn, Sie sagen dies. Gut geschriebener Code kann sich der Assemblersprache nähern.

Für C # / Java mit JIT-Kompilierung sind die ersten beiden niedrig, aber der gesammelte Speicherplatz ist mit Kosten verbunden. Gut geschriebener Code kann sich 2x C / C ++ nähern.

Für Python / Ruby / Perl sind die Kosten aller drei Faktoren relativ hoch. Denken Sie 5x im Vergleich zu C / C ++ oder schlechter. (*)

Denken Sie daran, dass der Code der Laufzeitbibliothek möglicherweise in derselben Sprache wie Ihre Programme geschrieben ist und dieselben Leistungseinschränkungen aufweist.


(*) Wenn die Just-In_Time (JIT) -Kompilierung auf diese Sprachen erweitert wird, nähern sich auch sie (normalerweise 2x) der Geschwindigkeit von gut geschriebenem C / C ++ - Code.

Es sollte auch beachtet werden, dass, sobald die Lücke eng ist (zwischen konkurrierenden Sprachen), Unterschiede von Algorithmen und Implementierungsdetails dominiert werden. JIT-Code schlägt möglicherweise C / C ++ und C / C ++ schlägt möglicherweise die Assemblersprache, weil es einfach einfacher ist, guten Code zu schreiben.

david.pfx
quelle
"Denken Sie daran, dass der Code der Laufzeitbibliothek möglicherweise in derselben Sprache wie Ihre Programme geschrieben ist und dieselben Leistungseinschränkungen aufweist." und "Für Python / Ruby / Perl sind die Kosten aller drei dieser Faktoren relativ hoch. Denken Sie 5x im Vergleich zu C / C ++ oder schlechter." Eigentlich stimmt das nicht. Zum Beispiel ist die Rubinius- HashKlasse (eine der Kerndatenstrukturen in Ruby) in Ruby geschrieben und arbeitet vergleichbar, manchmal sogar schneller als Hashdie in C geschriebene YARV- Klasse. Einer der Gründe ist, dass große Teile der Laufzeit von Rubinius System sind in Ruby geschrieben, damit sie…
Jörg W Mittag
… Zum Beispiel vom Rubinius-Compiler eingebunden werden. Extreme Beispiele sind die Klein VM (eine metacircular VM für Self) und die Maxine VM (eine metacircular VM für Java), in die alles geschrieben ist , sogar der Methodenversandcode, der Garbage Collector, der Speicherzuweiser, die primitiven Typen, die Kerndatenstrukturen und die Algorithmen Selbst oder Java. Auf diese Weise können sogar Teile der Kern-VM in den Benutzercode eingefügt werden, und die VM kann sich mithilfe des Laufzeit-Feedbacks aus dem Benutzerprogramm neu kompilieren und optimieren.
Jörg W Mittag
@ JörgWMittag: Immer noch wahr. Rubinius hat JIT, und JIT-Code schlägt C / C ++ bei einzelnen Benchmarks häufig. Ich kann keine Beweise dafür finden, dass dieses metacirculare Zeug in Abwesenheit von JIT viel zur Geschwindigkeit beiträgt. [Siehe Bearbeitung für Klarheit über JIT.]
david.pfx
1

Aber gibt es technische Einschränkungen oder Sprachfunktionen, die verhindern, dass mein Python-Skript so schnell ist wie ein gleichwertiges C ++ - Programm?

Nein. Es ist nur eine Frage des Geldes und der Ressourcen, die in die schnelle Ausführung von C ++ fließen, im Vergleich zu Geld und Ressourcen, die in die schnelle Ausführung von Python fließen.

Als beispielsweise die Self VM herauskam, war sie nicht nur die schnellste dynamische OO-Sprache, sondern auch die schnellste OO-Sprachperiode. Obwohl es sich um eine unglaublich dynamische Sprache handelt (viel mehr als beispielsweise Python, Ruby, PHP oder JavaScript), war sie schneller als die meisten verfügbaren C ++ - Implementierungen.

Aber dann hat Sun das Self-Projekt (eine ausgereifte OO-Allzwecksprache für die Entwicklung großer Systeme) abgebrochen, um sich auf eine kleine Skriptsprache für animierte Menüs in TV-Set-Top-Boxen zu konzentrieren (Sie haben vielleicht davon gehört, es heißt Java) mehr Finanzierung. Gleichzeitig haben Intel, IBM, Microsoft, Sun, Metrowerks, HP et al. hat viel Geld und Ressourcen ausgegeben, um C ++ schnell zu machen. CPU-Hersteller haben ihren Chips Funktionen hinzugefügt, um C ++ schnell zu machen. Betriebssysteme wurden geschrieben oder geändert, um C ++ schnell zu machen. C ++ ist also schnell.

Ich bin mit Python nicht sonderlich vertraut, ich bin eher eine Ruby-Person, daher werde ich ein Beispiel von Ruby geben: Die HashKlasse (entspricht in Funktion und Bedeutung der dictin Python) in der Rubinius Ruby-Implementierung ist in 100% reinem Ruby geschrieben; Dennoch konkurriert es günstig und übertrifft manchmal sogar die HashKlasse in YARV, die in handoptimiertem C geschrieben ist. Und im Vergleich zu einigen kommerziellen Lisp- oder Smalltalk-Systemen (oder der oben genannten Self-VM) ist Rubinius 'Compiler nicht einmal so clever .

Python enthält nichts, was es langsam macht. Es gibt Funktionen in heutigen Prozessoren und Betriebssystemen, die Python schaden (z. B. ist bekannt, dass der virtuelle Speicher für die Leistung der Speicherbereinigung schrecklich ist). Es gibt Funktionen, die C ++ helfen, aber Python nicht helfen (moderne CPUs versuchen, Cache-Fehler zu vermeiden, weil sie so teuer sind. Leider ist es schwierig, Cache-Fehler zu vermeiden, wenn Sie über OO und Polymorphismus verfügen. Stattdessen sollten Sie die Cache-Kosten senken vermisst. Die Azul Vega CPU, die für Java entwickelt wurde, macht das.)

Wenn Sie so viel Geld, Forschung und Ressourcen für die schnelle Erstellung von Python ausgeben wie für C ++, und Sie so viel Geld, Forschung und Ressourcen für die Erstellung von Betriebssystemen ausgeben, mit denen Python-Programme schnell ausgeführt werden, wie dies für C ++ getan wurde, und Sie wie für ausgeben Viel Geld, Forschung und Ressourcen für die Erstellung von CPUs, mit denen Python-Programme schnell ausgeführt werden, wie dies für C ++ der Fall war. Ich bin mir sicher, dass Python eine vergleichbare Leistung wie C ++ erzielen kann.

Wir haben mit ECMAScript gesehen, was passieren kann, wenn nur ein Spieler die Leistung ernst nimmt. Innerhalb eines Jahres konnten wir bei allen großen Anbietern eine 10-fache Leistungssteigerung erzielen.

Jörg W Mittag
quelle