Warum sind Pythons Arrays langsam?

153

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?

Valentin Lorentz
quelle
4
Numpy Tools können Ihr Array effizient ausnutzen:% timeit np.sum (A): 100 Schleifen, am besten 3: 8,87 ms pro Schleife
BM
6
Ich bin noch nie auf eine Situation gestoßen, in der ich das arrayPaket verwenden musste. Wenn Sie viel rechnen möchten, arbeitet Numpy mit Lichtgeschwindigkeit (dh C) und ist normalerweise besser als naive Implementierungen von Dingen wie sum()).
Nick T
40
Enge Wähler: Warum genau basiert diese Meinung? OP scheint eine spezifische technische Frage zu einem messbaren und wiederholbaren Phänomen zu stellen.
Kevin
5
@NickT Read Eine Optimierungsanekdote . Es stellt sich heraus, dass das arrayKonvertieren einer Ganzzahlfolge (die ASCII-Bytes darstellt) in ein strObjekt 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. numpyist viel besser für den Umgang mit Arrays, aber es ist eine Abhängigkeit von Drittanbietern.
Bakuriu

Antworten:

220

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- intObjekt. Das kostet Zeit. In Ihrem Fall sum(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:

PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
    /* error checking omitted */
    return ((PyListObject *)op) -> ob_item[i];
}

Es gibt sehr wenig zu tun: somelist[i]Gibt nur das i'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 entspricht struct PyObject).

Und hier ist die __getitem__Implementierung für einen arrayWith-Typ-Code l:

static PyObject *
l_getitem(arrayobject *ap, Py_ssize_t i)
{
    return PyLong_FromLong(((long *)ap->ob_item)[i]);
}

Der Rohspeicher wird als Vektor plattformnativer C longGanzzahlen behandelt. das i'th C longwird gelesen; und wird dann PyLong_FromLong()aufgerufen, um das native Element C longin ein Python- longObjekt zu verpacken ("box") (das in Python 3, das die Unterscheidung von Python 2 zwischen intund beseitigt long, tatsächlich als Typ angezeigt wird int).

Dieses Boxen muss neuen Speicher für ein Python- intObjekt zuweisen und die C longBits des Native darin sprühen . Im Kontext des ursprünglichen Beispiels ist die Lebensdauer dieses Objekts sehr kurz (gerade lang genug sum(), um den Inhalt zu einer laufenden Summe hinzuzufügen), und dann ist mehr Zeit erforderlich, um die Zuordnung des neuen intObjekts aufzuheben .

Hier kommt der Geschwindigkeitsunterschied her, ist immer hergekommen und wird immer von in der CPython-Implementierung kommen.

Tim Peters
quelle
87

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:

  1. Sie schreiben jetzt C anstelle von Python. Cython ist eine Möglichkeit, dies zu verbessern, beseitigt jedoch nicht viele grundlegende Unterschiede zwischen den Sprachen. Sie müssen mit der C-Semantik vertraut sein und verstehen, was sie tut.
  2. Die C-API von PyPy funktioniert bis zu einem gewissen Grad , ist aber nicht sehr schnell. Wenn Sie auf PyPy abzielen, sollten Sie wahrscheinlich nur einfachen Code mit regulären Listen schreiben und ihn dann vom JITter für Sie optimieren lassen.
  3. C-Erweiterungen sind schwieriger zu verteilen als reiner Python-Code, da sie kompiliert werden müssen. Die Kompilierung hängt in der Regel von der Architektur und dem Betriebssystem ab. Sie müssen daher sicherstellen, dass Sie für Ihre Zielplattform kompilieren.

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.

Kevin
quelle
10

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):

import numpy
import array
L = list(range(10**7))
A = array.array('l', L)
N = numpy.array(L)

%timeit sum(L)
10 loops, best of 3: 101 ms per loop

%timeit sum(A)
1 loop, best of 3: 237 ms per loop

%timeit sum(N)
1 loop, best of 3: 743 ms per loop

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:

%timeit N.sum()
100 loops, best of 3: 6.27 ms per loop

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

%timeit list(range(10**7))
1 loop, best of 3: 283 ms per loop

%timeit array.array('l', range(10**7))
1 loop, best of 3: 884 ms per loop

%timeit numpy.array(range(10**7))
1 loop, best of 3: 1.49 s per loop

%timeit numpy.arange(10**7)
10 loops, best of 3: 21.7 ms per loop

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:

sys.getsizeof(L)
90000112
sys.getsizeof(A)
81940352
sys.getsizeof(N)
80000096

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.

N=numpy.arange(10**7, dtype=numpy.int32)

sys.getsizeof(N)
40000096

%timeit N.sum()
100 loops, best of 3: 8.35 ms per loop

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.

Robin Roth
quelle
-1

Bitte beachten Sie, dass 100000000auf gleich 10^8nicht zu 10^7, und meine Ergebnisse sind als folowwing:

100000000 == 10**8

# my test results on a Linux virtual machine:
#<L = list(range(100000000))> Time: 0:00:03.263585
#<A = array.array('l', range(100000000))> Time: 0:00:16.728709
#<L = list(range(10**8))> Time: 0:00:03.119379
#<A = array.array('l', range(10**8))> Time: 0:00:18.042187
#<A = array.array('l', L)> Time: 0:00:07.524478
#<sum(L)> Time: 0:00:01.640671
#<np.sum(L)> Time: 0:00:20.762153
S. Cheraghifar
quelle