2017-09-12 3 views
6

Ich habe versucht, eine reine Python (ohne externe Abhängigkeiten) elementweisen Vergleich von zwei Sequenzen zu machen. Meine erste Lösung war:Leistung der Karte vs Starmap?

list(map(operator.eq, seq1, seq2)) 

Dann fand ich starmap Funktion von itertools, was mir ziemlich ähnlich zu sein schien. Aber es stellte sich heraus, dass es im schlimmsten Fall 37% schneller auf meinem Computer war. Da es nicht mir klar war, habe ich die Zeit, die notwendig sind 1-Element von einem Generator zum Abrufen (weiß nicht, ob diese Art und Weise richtig ist):

from operator import eq 
from itertools import starmap 

seq1 = [1,2,3]*10000 
seq2 = [1,2,3]*10000 
seq2[-1] = 5 

gen1 = map(eq, seq1, seq2)) 
gen2 = starmap(eq, zip(seq1, seq2)) 

%timeit -n1000 -r10 next(gen1) 
%timeit -n1000 -r10 next(gen2) 

271 ns ± 1.26 ns per loop (mean ± std. dev. of 10 runs, 1000 loops each) 
208 ns ± 1.72 ns per loop (mean ± std. dev. of 10 runs, 1000 loops each) 

Elemente der zweiten Lösung 24% performanter In Abrufen ist . Danach ergeben beide die gleichen Ergebnisse für list. Aber von irgendwo gewinnen wir zusätzliches 13% in der Zeit:

%timeit list(map(eq, seq1, seq2)) 
%timeit list(starmap(eq, zip(seq1, seq2))) 

5.24 ms ± 29.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 
3.34 ms ± 84.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 

Ich weiß nicht, wie tiefer solchen verschachtelten Code in Profilierung zu graben? Also meine Frage warum ist der erste Generator so schneller beim Abrufen und woher bekommen wir extra 13% in list Funktion?

EDIT: Meine erste Absicht war, elementweise Vergleich statt all, durchzuführen, um die all Funktion mit list ersetzt wurde. Dieser Austausch hat keinen Einfluss auf das Timing-Verhältnis.

CPython 3.6.2 unter Windows 10 (64-Bit)

+1

Warum nicht einfach verwenden 'seq1 = = seq2'? –

+0

@ Błotosmętek danke für die Korrektur! Meine erste Absicht war der elementweise Vergleich anstelle von 'all', was aus meiner Frage nicht ersichtlich war :) Wirklich, wenn Sie' list' statt 'all' ersetzen, wird die Reihenfolge der Zeiten gleich sein. – godaygo

+0

Welche Python-Version? Und ist das CPython? – MSeifert

Antwort

2

Es gibt mehrere Faktoren, die (in Verbindung) Beitrag zur Performance-Unterschied beobachtet:

  • zip wiederverwendet, die zurück tuple, wenn sie einen Referenzzähler von 1 hat, wenn der nächste __next__ Aufruf ist gemacht.
  • map baut ein neuetuple, die auf die „abgebildet Funktion“ jedes Mal, wenn ein __next__ Anruf erfolgt, übergeben wird. Eigentlich wird es wahrscheinlich kein neues Tupel von Grund auf neu erstellen, da Python einen Speicher für nicht verwendete Tupeln hält. Aber in diesem Fall map hat ein unbenutztes Tupel in der richtigen Größe zu finden.
  • starmap prüft, ob das nächste Element in den iterable vom Typ tuple und wenn ja, es gibt sie nur an.
  • Aufruf einer C-Funktion aus C-Code mit PyObject_Call kein neues Tupel erstellen, die an den Angerufenen übergeben wird.

So starmap mit zip wird ein Tupel nur immer und immer wieder verwenden, dass so zu operator.eq geben wird immens den Funktionsaufruf Overhead reduziert. map auf der anderen Seite wird jedes Mal, wenn operator.eq aufgerufen wird, ein neues Tupel erstellen (oder füllen Sie ein C-Array von CPython 3.6 an). Was also eigentlich der Geschwindigkeitsunterschied ist, ist nur der Tupelerstellungsoverhead.

Statt auf den Quellcode der Verknüpfung werde ich einige Cython Code, der verwendet werden kann, um dies zu überprüfen:

In [1]: %load_ext cython 

In [2]: %%cython 
    ...: 
    ...: from cpython.ref cimport Py_DECREF 
    ...: 
    ...: cpdef func(zipper): 
    ...:  a = next(zipper) 
    ...:  print('a', a) 
    ...:  Py_DECREF(a) 
    ...:  b = next(zipper) 
    ...:  print('a', a) 

In [3]: func(zip([1, 2], [1, 2])) 
a (1, 1) 
a (2, 2) 

Ja, tuple s sind nicht wirklich unveränderlich, ein einfaches Py_DECREF war ausreichend, um " trick "zip zu glauben, dass niemand sonst einen Verweis auf das zurückgegebene Tupel enthält!

Wie für den „Tupel-Pass-Thru“:

In [4]: %%cython 
    ...: 
    ...: def func_inner(*args): 
    ...:  print(id(args)) 
    ...: 
    ...: def func(*args): 
    ...:  print(id(args)) 
    ...:  func_inner(*args) 

In [5]: func(1, 2) 
1404350461320 
1404350461320 

So das Tupel übergeben wird rechts durch (nur weil diese als C-Funktionen definiert werden!) Dies geschieht nicht für reine Python-Funktionen:

In [6]: def func_inner(*args): 
    ...:  print(id(args)) 
    ...: 
    ...: def func(*args): 
    ...:  print(id(args)) 
    ...:  func_inner(*args) 
    ...: 

In [7]: func(1, 2) 
1404350436488 
1404352833800 

Beachten sie, dass es auch nicht passieren, wenn die aufgerufene Funktion nicht eine C-Funktion ist auch dann, wenn aus einer C-Funktion aufgerufen:

In [8]: %%cython 
    ...: 
    ...: def func_inner_c(*args): 
    ...:  print(id(args)) 
    ...: 
    ...: def func(inner, *args): 
    ...:  print(id(args)) 
    ...:  inner(*args) 
    ...: 

In [9]: def func_inner_py(*args): 
    ...:  print(id(args)) 
    ...: 
    ...: 

In [10]: func(func_inner_py, 1, 2) 
1404350471944 
1404353010184 

In [11]: func(func_inner_c, 1, 2) 
1404344354824 
1404344354824 

So gibt es eine Menge „Zufälle“ sind bis zu dem Punkt, dass im Vorfeld starmap mit zip ist schneller als map mit mehreren Argumenten aufrufen, wenn die aufgerufene Funktion auch eine C-Funktion ist ...

1

Ein Unterschied, den ich feststellen kann, ist das, wie map Elemente aus dem Iterables abruft. Sowohl map als auch zip erzeugen ein Tupel von Iteratoren von jedem iterierbaren Wert. Jetzt zip unterhält ein result tuple intern die nächste jedes Mal aufgefüllt wird aufgerufen und auf der anderen Seite, map creates a new array* mit jedem nächsten Anruf und es freigibt.


* Wie von MSeifert bis 3.5.4 map_next verwendet ein neues Python-Tupel jedes Mal zuzuweisen. Dies wurde in 3.6 geändert und bis 5 iterables C-Stack verwendet und für alles, was größer als dieser Heap ist, verwendet. Verwandte PRs: Issue #27809: map_next() uses fast call und Add _PY_FASTCALL_SMALL_STACK constant | Problem: https://bugs.python.org/issue27809

+0

Das würde annehmen, dass dies 3,6 ist, beachten Sie, dass der Code in [3.5.4] (https://github.com/python/cpython/blob/v3 .5.4/Python/bltinmodule.C# L1168-L1192) sieht anders aus. :) – MSeifert

+0

@MSeifert Ich frage mich, wie viel langsam/schnell 3.5.4 Implementierung wurde mit 3.6.2 verglichen. –

+0

(sollte langsam bis 5 iterables wegen Heap vs C-Stack-Zugriff sein) –