Ich hatte erwartet array.array
, schneller als Listen zu sein, da Arrays scheinbar nicht in der Box sind.
Ich erhalte jedoch folgendes Ergebnis:
In [1]: import array
In [2]: L = list(range(100000000))
In [3]: A = array.array('l', range(100000000))
In [4]: %timeit sum(L)
1 loop, best of 3: 667 ms per loop
In [5]: %timeit sum(A)
1 loop, best of 3: 1.41 s per loop
In [6]: %timeit sum(L)
1 loop, best of 3: 627 ms per loop
In [7]: %timeit sum(A)
1 loop, best of 3: 1.39 s per loop
Was könnte die Ursache für einen solchen Unterschied sein?
python
arrays
performance
boxing
python-internals
Valentin Lorentz
quelle
quelle
array
Paket verwenden musste. Wenn Sie viel rechnen möchten, arbeitet Numpy mit Lichtgeschwindigkeit (dh C) und ist normalerweise besser als naive Implementierungen von Dingen wiesum()
).array
Konvertieren einer Ganzzahlfolge (die ASCII-Bytes darstellt) in einstr
Objekt ziemlich schnell ist . Guido selbst kam erst nach vielen anderen Lösungen auf diese Idee und war von der Leistung ziemlich überrascht. Jedenfalls ist dies der einzige Ort, an dem ich mich daran erinnere, dass es nützlich war.numpy
ist viel besser für den Umgang mit Arrays, aber es ist eine Abhängigkeit von Drittanbietern.Antworten:
Der Speicher ist "unboxed", aber jedes Mal, wenn Sie auf ein Element zugreifen, muss Python es "boxen" (in ein reguläres Python-Objekt einbetten), um etwas damit zu tun. Beispielsweise
sum(A)
iterieren Sie über das Array und boxen jede Ganzzahl einzeln in einem regulären Python-int
Objekt. Das kostet Zeit. In Ihrem Fallsum(L)
wurde das gesamte Boxen zum Zeitpunkt der Erstellung der Liste durchgeführt.Letztendlich ist ein Array also im Allgemeinen langsamer, benötigt jedoch wesentlich weniger Speicher.
Hier ist der relevante Code aus einer aktuellen Version von Python 3, aber die gleichen Grundideen gelten für alle CPython-Implementierungen seit der ersten Veröffentlichung von Python.
Hier ist der Code für den Zugriff auf ein Listenelement:
Es gibt sehr wenig zu tun:
somelist[i]
Gibt nur dasi
'th Objekt in der Liste zurück (und alle Python-Objekte in CPython sind Zeiger auf eine Struktur, deren anfängliches Segment dem Layout von a entsprichtstruct PyObject
).Und hier ist die
__getitem__
Implementierung für einenarray
With-Typ-Codel
:Der Rohspeicher wird als Vektor plattformnativer
C
long
Ganzzahlen behandelt. dasi
'thC long
wird gelesen; und wird dannPyLong_FromLong()
aufgerufen, um das native ElementC long
in ein Python-long
Objekt zu verpacken ("box") (das in Python 3, das die Unterscheidung von Python 2 zwischenint
und beseitigtlong
, tatsächlich als Typ angezeigt wirdint
).Dieses Boxen muss neuen Speicher für ein Python-
int
Objekt zuweisen und dieC long
Bits des Native darin sprühen . Im Kontext des ursprünglichen Beispiels ist die Lebensdauer dieses Objekts sehr kurz (gerade lang genugsum()
, um den Inhalt zu einer laufenden Summe hinzuzufügen), und dann ist mehr Zeit erforderlich, um die Zuordnung des neuenint
Objekts aufzuheben .Hier kommt der Geschwindigkeitsunterschied her, ist immer hergekommen und wird immer von in der CPython-Implementierung kommen.
quelle
Um die hervorragende Antwort von Tim Peters zu ergänzen, implementieren Arrays das Pufferprotokoll , Listen jedoch nicht. Dies bedeutet, dass Sie, wenn Sie eine C-Erweiterung (oder ein moralisches Äquivalent wie das Schreiben eines Cython- Moduls) schreiben , viel schneller auf die Elemente eines Arrays zugreifen und mit ihnen arbeiten können als alles, was Python tun kann. Dies führt zu erheblichen Geschwindigkeitsverbesserungen, möglicherweise weit über eine Größenordnung. Es hat jedoch eine Reihe von Nachteilen:
Wenn Sie direkt zu C-Erweiterungen wechseln, können Sie je nach Anwendungsfall einen Vorschlaghammer verwenden, um eine Fliege zu schlagen. Sie sollten zuerst NumPy untersuchen und herausfinden, ob es leistungsfähig genug ist, um die Mathematik auszuführen, die Sie versuchen. Bei korrekter Verwendung ist es auch viel schneller als natives Python.
quelle
Tim Peters antwortete, warum dies langsam ist, aber mal sehen, wie man es verbessern kann.
Halten Sie sich an Ihr Beispiel von
sum(range(...))
(Faktor 10 kleiner als Ihr Beispiel, um hier in den Speicher zu passen):Auf diese Weise muss auch numpy boxen / entpacken, was zusätzlichen Overhead hat. Um es schnell zu machen, muss man innerhalb des numpy c-Codes bleiben:
Von der Listenlösung bis zur Numpy-Version ist dies ein Faktor 16 in der Laufzeit.
Lassen Sie uns auch überprüfen, wie lange das Erstellen dieser Datenstrukturen dauert
Klarer Gewinner: Numpy
Beachten Sie auch, dass das Erstellen der Datenstruktur ungefähr so viel Zeit wie das Summieren benötigt, wenn nicht mehr. Das Zuweisen von Speicher ist langsam.
Speichernutzung von diesen:
Diese benötigen also 8 Bytes pro Nummer mit unterschiedlichem Overhead. Für den Bereich, den wir verwenden, sind 32-Bit-Ints ausreichend, damit wir etwas Speicher sichern können.
Es stellt sich jedoch heraus, dass das Hinzufügen von 64-Bit-Ints auf meinem Computer schneller ist als 32-Bit-Ints. Dies lohnt sich also nur, wenn Sie durch Speicher / Bandbreite begrenzt sind.
quelle
Bitte beachten Sie, dass
100000000
auf gleich10^8
nicht zu10^7
, und meine Ergebnisse sind als folowwing:quelle