wake-up-neo.net

Warum ist ein Listenverständnis so viel schneller als das Anhängen an eine Liste?

Ich habe mich gefragt, warum das Listenverständnis so viel schneller ist als das Anhängen an eine Liste. Ich dachte, der Unterschied ist nur ausdrucksstark, aber nicht.

>>> import timeit 
>>> timeit.timeit(stmt='''\
t = []
for i in range(10000):
    t.append(i)''', number=10000)
9.467898777974142

>>> timeit.timeit(stmt='t= [i for i in range(10000)]', number=10000)
4.1138417314859

Das Listenverständnis ist 50% schneller. Warum?

35
rafaelc

Das Listenverständnis ist im Grunde genommen nur ein "syntaktischer Zucker" für die reguläre for -Schleife. In diesem Fall ist die bessere Leistung darauf zurückzuführen, dass das Append-Attribut der Liste nicht geladen und bei jeder Iteration als Funktion aufgerufen werden muss. Mit anderen Worten und im Allgemeinen ist das Listenverständnis schneller, da das Anhalten und Fortsetzen eines Funktionsrahmens oder mehrerer Funktionen in anderen Fällen langsamer ist als das Erstellen einer Liste bei Bedarf.

Betrachten Sie die folgenden Beispiele:

# Python-3.6

In [1]: import dis

In [2]: def f1():
   ...:     l = []
   ...:     for i in range(5):
   ...:         l.append(i)
   ...:         

In [3]: def f2():
   ...:     [i for i in range(5)]
   ...:     

In [4]: dis.dis(f1)
  2           0 BUILD_LIST               0
              3 STORE_FAST               0 (l)

  3           6 SETUP_LOOP              33 (to 42)
              9 LOAD_GLOBAL              0 (range)
             12 LOAD_CONST               1 (5)
             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             18 GET_ITER
        >>   19 FOR_ITER                19 (to 41)
             22 STORE_FAST               1 (i)

  4          25 LOAD_FAST                0 (l)
             28 LOAD_ATTR                1 (append)
             31 LOAD_FAST                1 (i)
             34 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             37 POP_TOP
             38 JUMP_ABSOLUTE           19
        >>   41 POP_BLOCK
        >>   42 LOAD_CONST               0 (None)
             45 RETURN_VALUE

In [5]: dis.dis(f2)
  2           0 LOAD_CONST               1 (<code object <listcomp> at 0x7fe48b2265d0, file "<ipython-input-3-9bc091d521d5>", line 2>)
              3 LOAD_CONST               2 ('f2.<locals>.<listcomp>')
              6 MAKE_FUNCTION            0
              9 LOAD_GLOBAL              0 (range)
             12 LOAD_CONST               3 (5)
             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             18 GET_ITER
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 POP_TOP
             23 LOAD_CONST               0 (None)
             26 RETURN_VALUE

Sie können an Offset 22 sehen, dass wir in der ersten Funktion ein append -Attribut haben, da wir in der zweiten Funktion unter Verwendung des Listenverständnisses kein solches haben. All diese zusätzlichen Bytecodes verlangsamen den Ansatz beim Anhängen. Beachten Sie auch, dass Sie in jeder Iteration auch das append -Attribut laden müssen, wodurch Ihr Code bei Verwendung des Listenverständnisses ungefähr 2 Mal langsamer als die zweite Funktion dauert.

67
Kasrâmvd

Selbst wenn man die Zeit herausrechnet, die zum Nachschlagen und Laden der Funktion append benötigt wird, ist das Listenverständnis immer noch schneller, da die Liste in C erstellt wird, anstatt in Python jeweils ein Element aufzubauen.

# Slow
timeit.timeit(stmt='''
    for i in range(10000):
        t.append(i)''', setup='t=[]', number=10000)

# Faster
timeit.timeit(stmt='''
    for i in range(10000):
        l(i)''', setup='t=[]; l=t.append', number=10000)

# Faster still
timeit.timeit(stmt='t = [i for i in range(10000)]', number=10000)
12
chepner

Unter Berufung auf this article wird das append -Attribut von list nicht als Funktion gesucht, geladen und aufgerufen, was Zeit in Anspruch nimmt und sich summiert über Iterationen.

6
adarsh