19 Stimmen

Leistung von List Comprehension, Map und numpy.vectorize

Ich habe eine Funktion foo(i), die eine ganze Zahl annimmt und eine beträchtliche Menge an Zeit zur Ausführung benötigt. Gibt es einen signifikanten Leistungsunterschied zwischen einer der folgenden Möglichkeiten der Initialisierung a :

a = [foo(i) for i in xrange(100)]

a = map(foo, range(100))

vfoo = numpy.vectorize(foo)
a = vfoo(range(100))

(Mir ist es egal, ob die Ausgabe eine Liste oder ein Numpy-Array ist).

Gibt es einen besseren Weg?

26voto

Mike Graham Punkte 68846
  • Warum optimieren Sie das? Haben Sie funktionierenden, getesteten Code geschrieben und dann Ihren Algorithmus untersucht? profiliert und festgestellt, dass die Optimierung dieses Codes eine Wirkung hat? Tun Sie dies in einer tiefen inneren Schleife, in der Sie Ihre Zeit verbringen? Wenn nicht, lassen Sie es bleiben.

  • Sie werden nur dann wissen, was für Sie am schnellsten funktioniert, wenn Sie die Zeit dafür nehmen. Um die Zeitmessung sinnvoll zu gestalten, müssen Sie sie auf Ihren tatsächlichen Anwendungsfall abstimmen. So können Sie beispielsweise deutliche Leistungsunterschiede zwischen einem Funktionsaufruf in einem Listenverständnis und einem Inline-Ausdruck feststellen; es ist nicht klar, ob Sie wirklich Ersteres wollten oder ob Sie es darauf reduziert haben, um Ihre Fälle ähnlich zu machen.

  • Sie sagen, dass es keine Rolle spielt, ob Sie am Ende ein Numpy-Array oder ein list aber wenn man diese Art von Mikro-Optimierung vornimmt, ist es hace Das spielt keine Rolle, da sie sich bei der späteren Verwendung anders verhalten. Es könnte schwierig sein, den Finger darauf zu legen, also wird sich hoffentlich herausstellen, dass das ganze Problem vorzeitig hinfällig ist.

  • In der Regel ist es besser, einfach das richtige Werkzeug für die jeweilige Aufgabe zu verwenden, um Klarheit, Lesbarkeit usw. zu gewährleisten. Es ist selten, dass mir die Entscheidung zwischen diesen Dingen schwer fällt.

    • Wenn ich Numpy-Arrays bräuchte, würde ich sie verwenden. Ich würde sie für die Speicherung großer homogener Arrays oder multidimensionaler Daten verwenden. Ich verwende sie häufig, aber nur selten, wenn ich eine Liste verwenden möchte.
      • Wenn ich diese verwenden würde, würde ich mein Bestes tun, um meine Funktionen zu schreiben bereits vektorisiert, so dass ich nicht mit numpy.vectorize . Zum Beispiel, times_five kann auf ein Numpy-Array ohne Dekoration angewendet werden.
    • Wenn ich keinen Grund hätte, numpy zu verwenden, d.h. wenn ich keine numerischen mathematischen Probleme lösen oder spezielle numpy-Funktionen verwenden oder mehrdimensionale Arrays speichern würde oder was auch immer...
      • Hätte ich eine bereits existierende Funktion, würde ich verwenden map . Dafür ist es da.
      • Wenn ich eine Operation hätte, die in einen kleinen Ausdruck passt und ich keine Funktion bräuchte, würde ich eine Listenverarbeitung verwenden.
      • Wenn ich nur die Operation für alle Fälle durchführen wollte, aber das Ergebnis nicht speichern müsste, würde ich eine einfache for-Schleife verwenden.
      • In vielen Fällen würde ich sogar map und die faulen Äquivalente der Listenauffassungen: itertools.imap und Generatorausdrücke. Diese können den Speicherverbrauch um einen Faktor von n in einigen Fällen und kann die Durchführung unnötiger Operationen manchmal vermeiden.

Wenn sich herausstellt, dass hier die Leistungsprobleme liegen, ist es schwierig, diese Dinge richtig zu machen. Es ist sehr Es kommt häufig vor, dass Menschen für ihre tatsächlichen Probleme den falschen Spielzeugkoffer wählen. Schlimmer noch, es ist extrem üblich, dass die Leute daraus dumme allgemeine Regeln ableiten.

Betrachten Sie die folgenden Fälle (timeme.py wird unten veröffentlicht)

python -m timeit "from timeme import x, times_five; from numpy import vectorize" "vectorize(times_five)(x)"
1000 loops, best of 3: 924 usec per loop

python -m timeit "from timeme import x, times_five" "[times_five(item) for item in x]"
1000 loops, best of 3: 510 usec per loop

python -m timeit "from timeme import x, times_five" "map(times_five, x)"
1000 loops, best of 3: 484 usec per loop

Ein naiver Beobachter würde zu dem Schluss kommen, dass map die leistungsstärkste dieser Optionen ist, aber die Antwort lautet immer noch "es kommt darauf an". Bedenken Sie die Vorteile der von Ihnen verwendeten Tools: Mit List Comprehensions können Sie die Definition einfacher Funktionen vermeiden; mit Numpy können Sie Dinge in C vektorisieren, wenn Sie die richtigen Dinge tun.

python -m timeit "from timeme import x, times_five" "[item + item + item + item + item for item in x]"
1000 loops, best of 3: 285 usec per loop

python -m timeit "import numpy; x = numpy.arange(1000)" "x + x + x + x + x"
10000 loops, best of 3: 39.5 usec per loop

Aber das ist nicht alles - es gibt noch mehr. Bedenken Sie die Macht einer Algorithmusänderung. Sie kann sogar noch dramatischer sein.

python -m timeit "from timeme import x, times_five" "[5 * item for item in x]"
10000 loops, best of 3: 147 usec per loop

python -m timeit "import numpy; x = numpy.arange(1000)" "5 * x"
100000 loops, best of 3: 16.6 usec per loop

Manchmal kann eine Änderung des Algorithmus sogar noch effektiver sein. Dies wird immer effektiver, je größer die Zahlen werden.

python -m timeit "from timeme import square, x" "map(square, x)"
10 loops, best of 3: 41.8 msec per loop

python -m timeit "from timeme import good_square, x" "map(good_square, x)"
1000 loops, best of 3: 370 usec per loop

Und selbst jetzt mag das alles wenig mit Ihrem eigentlichen Problem zu tun haben. Es sieht so aus, als ob numpy so großartig ist, wenn man es richtig einsetzen kann, aber es hat seine Grenzen: Keines dieser numpy-Beispiele verwendet echte Python-Objekte in den Arrays. Das verkompliziert das, was getan werden muss, sogar sehr. Und was ist, wenn wir doch C-Datentypen verwenden? Diese sind weniger robust als Python-Objekte. Sie sind nicht nullbar. Die Ganzzahlen laufen über. Man muss etwas zusätzliche Arbeit leisten, um sie abzurufen. Sie sind statisch typisiert. Manchmal erweisen sich diese Dinge als Probleme, sogar als unerwartete Probleme.

Da haben Sie es also: eine endgültige Antwort. "Es kommt darauf an."


# timeme.py

x = xrange(1000)

def times_five(a):
    return a + a + a + a + a

def square(a):
    if a == 0:
        return 0

    value = a
    for i in xrange(a - 1):
        value += a
    return value

def good_square(a):
    return a ** 2

13voto

wescpy Punkte 9858

Erste Bemerkung: Verwenden Sie nicht gleichzeitig die xrange( ) oder range() in Ihren Stichproben... Damit ist Ihre Frage hinfällig, da Sie Äpfel mit Birnen vergleichen.

Ich unterstütze @Gabe's Vorstellung, dass, wenn Sie viele große Datenstrukturen haben, Numpy insgesamt gewinnen sollte ... nur im Hinterkopf behalten die meiste Zeit C ist schneller als Python, aber dann wieder, die meiste Zeit, PyPy ist schneller als CPython. :-)

Was den Vergleich von Listcomps und map() Aufrufe gehen... einer macht 101 Funktionsaufrufe, der andere 102. Das bedeutet, dass Sie keinen signifikanten Unterschied im Timing sehen werden, wie unten mit der timeit Modul als @Mike vorgeschlagen hat:

  • Liste Verstehen

    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    1000000 loops, best of 3: 0.216 usec per loop
    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    1000000 loops, best of 3: 0.21 usec per loop
    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    1000000 loops, best of 3: 0.212 usec per loop

  • map() Funktionsaufruf

    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    1000000 loops, best of 3: 0.216 usec per loop
    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    1000000 loops, best of 3: 0.214 usec per loop
    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    1000000 loops, best of 3: 0.215 usec per loop

Allerdings sollten Sie, sofern Sie nicht vorhaben mit die Listen, die Sie mit einer dieser Techniken erstellen, versuchen Sie, sie (die Verwendung von Listen) ganz zu vermeiden. D.h., wenn alles, was Sie tun, ist Iteration über sie, es ist nicht wert, den Speicherverbrauch (und möglicherweise die Erstellung einer potenziell riesigen Liste im Speicher), wenn Sie nur kümmern, um jedes Element ein zu einer Zeit nur verwerfen die Liste, sobald Sie fertig sind.

In solchen Fällen empfehle ich dringend die Verwendung von Generatorausdrücke stattdessen, da sie nicht die gesamte Liste im Speicher erstellen... es ist eine speicherfreundliche, träge iterative Art der Schleife durch Elemente zu verarbeiten, ohne ein großes Array im Speicher zu erstellen. Das Beste daran ist, dass die Syntax fast identisch mit der von listcomps ist:

a = (foo(i) for i in range(100))

Nur 2.x-Benutzer : im Sinne von mehr Iteration, Änderung aller range() Anrufe an xrange() für alle älteren 2.x-Codes und wechseln Sie dann zu range() bei der Portierung auf Python 3, wo xrange() ersetzt und wird umbenannt in range() .

7voto

Gabe Punkte 82268

Wenn die Funktion selbst eine beträchtliche Zeit zur Ausführung benötigt, ist es unerheblich, wie Sie ihre Ausgabe einem Array zuordnen. Sobald Sie jedoch in Arrays mit Millionen von Zahlen einsteigen, können Sie mit numpy eine erhebliche Menge an Speicher sparen.

4voto

Justin Peel Punkte 46114

Die Liste Verständnis ist die schnellste, dann die Karte, dann die Numpy auf meiner Maschine. Der numpy-Code ist tatsächlich etwas langsamer als die anderen beiden, aber der Unterschied ist viel geringer, wenn man numpy.arange anstelle von range (oder xrange) verwendet, wie ich es in den unten aufgeführten Zeiten tat. Wenn Sie psyco verwenden, wird auch die Listenverarbeitung beschleunigt, während die beiden anderen für mich langsamer waren. Ich habe auch größere Zahlenarrays als in Ihrem Code verwendet und meine foo-Funktion hat nur die Quadratwurzel berechnet. Hier sind einige typische Zeiten.

Ohne Psyco:

list comprehension: 47.5581952455 ms
map: 51.9082732582 ms
numpy.vectorize: 57.9601876775 ms

Mit Psyco:

list comprehension: 30.4318844993 ms
map: 96.4504427239 ms
numpy.vectorize: 99.5858691538 ms

Ich habe Python 2.6.4 und das Modul timeit verwendet.

Auf der Grundlage dieser Ergebnisse würde ich sagen, dass es wahrscheinlich keinen Unterschied macht, welche Methode Sie für die Initialisierung wählen. Ich würde mich aufgrund der Geschwindigkeit wahrscheinlich für die Numpy- oder die List-Comprehension entscheiden, aber letztendlich sollten Sie sich bei Ihrer Wahl davon leiten lassen, was Sie anschließend mit dem Array machen wollen.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X