Warum ist das Schleifen über range () in Python schneller als das Verwenden einer while-Schleife?

81

Neulich habe ich Python-Benchmarking durchgeführt und bin auf etwas Interessantes gestoßen. Unten sind zwei Schleifen aufgeführt, die mehr oder weniger dasselbe tun. Die Ausführung von Schleife 1 dauert ungefähr doppelt so lange wie Schleife 2.

Schleife 1:

int i = 0
while i < 100000000:
  i += 1

Schleife 2:

for n in range(0,100000000):
  pass

Warum ist die erste Schleife so viel langsamer? Ich weiß, dass es ein triviales Beispiel ist, aber es hat mein Interesse geweckt. Gibt es etwas Besonderes an der Funktion range (), das sie effizienter macht als das Inkrementieren einer Variablen auf dieselbe Weise?

A. Dorton
quelle

Antworten:

158

Wenn Sie den Python-Byte-Code zerlegen, erhalten Sie möglicherweise eine konkretere Vorstellung

Verwenden Sie while-Schleife:

1           0 LOAD_CONST               0 (0)
            3 STORE_NAME               0 (i)

2           6 SETUP_LOOP              28 (to 37)
      >>    9 LOAD_NAME                0 (i)              # <-
           12 LOAD_CONST               1 (100000000)      # <-
           15 COMPARE_OP               0 (<)              # <-
           18 JUMP_IF_FALSE           14 (to 35)          # <-
           21 POP_TOP                                     # <-

3          22 LOAD_NAME                0 (i)              # <-
           25 LOAD_CONST               2 (1)              # <-
           28 INPLACE_ADD                                 # <-
           29 STORE_NAME               0 (i)              # <-
           32 JUMP_ABSOLUTE            9                  # <-
      >>   35 POP_TOP
           36 POP_BLOCK

Der Schleifenkörper hat 10 op

Verwendungsbereich:

1           0 SETUP_LOOP              23 (to 26)
            3 LOAD_NAME                0 (range)
            6 LOAD_CONST               0 (0)
            9 LOAD_CONST               1 (100000000)
           12 CALL_FUNCTION            2
           15 GET_ITER
      >>   16 FOR_ITER                 6 (to 25)        # <-
           19 STORE_NAME               1 (n)            # <-

2          22 JUMP_ABSOLUTE           16                # <-
      >>   25 POP_BLOCK
      >>   26 LOAD_CONST               2 (None)
           29 RETURN_VALUE

Der Schleifenkörper hat 3 op

Die Zeit zum Ausführen von C-Code ist viel kürzer als beim Interpretieren und kann ignoriert werden.

kcwu
quelle
2
Tatsächlich hat der Schleifenkörper bei der ersten Demontage 10 Operationen (der Sprung von Position 32 auf 9). In der aktuellen CPython-Implementierung führt die Interpretation jedes Bytecodes mit einer ziemlich hohen Wahrscheinlichkeit zu einer kostspieligen Fehlvorhersage der indirekten Verzweigung in der CPU (dem Sprung zur Implementierung des nächsten Bytecodes). Dies ist eine Folge der aktuellen Implementierung von CPython. Die JITs, die von unbeladenem Swallow, PyPy und anderen implementiert werden, verlieren höchstwahrscheinlich diesen Overhead. Die besten von ihnen werden auch in der Lage sein, eine Typenspezialisierung für eine Beschleunigung um eine Größenordnung durchzuführen.
Ants Aasma
5
Verwenden Sie das Modul "dis". Definieren Sie Ihren Code in einer Funktion und rufen Sie dann dis.disco (func .__ code__)
kcwu
Wäre es dann richtig zu sagen, dass auf einer höheren Ebene eine whileSchleife bei jeder Iteration einen Vergleich durchführen muss?
Davidhood2
35

range()wird in C implementiert, während i += 1interpretiert wird.

Die Verwendung xrange()könnte es für große Zahlen noch schneller machen. Das Starten mit Python 3.0 range()ist das gleiche wie zuvor xrange().

Georg Schölly
quelle
15

Es muss gesagt werden, dass mit der while-Schleife viel Objekt erstellt und zerstört wird.

i += 1

ist das gleiche wie:

i = i + 1

Da Python-Ints jedoch unveränderlich sind, wird das vorhandene Objekt nicht geändert. Vielmehr entsteht ein brandneues Objekt mit einem neuen Wert. Es ist im Grunde:

i = new int(i + 1)   # Using C++ or Java-ish syntax

Der Garbage Collector muss auch eine große Menge an Aufräumarbeiten durchführen. "Objekterstellung ist teuer".

Peter
quelle
4

Weil Sie häufiger mit Code arbeiten, der im Interpretor in C geschrieben ist. dh i + = 1 ist in Python so langsam (vergleichsweise), während der Bereich (0, ...) ein C-Aufruf ist, den die for-Schleife meistens auch in C ausführt.

John Montgomery
quelle
1

Die meisten in Python integrierten Methodenaufrufe werden als C-Code ausgeführt. Code, der interpretiert werden muss, ist viel langsamer. In Bezug auf Speichereffizienz und Ausführungsgeschwindigkeit ist der Unterschied gigantisch. Die Python-Interna wurden extrem optimiert, und es ist am besten, diese Optimierungen zu nutzen.

Oh mein Gott
quelle
0

Ich denke, die Antwort hier ist etwas subtiler als die anderen Antworten vermuten lassen, obwohl das Wesentliche richtig ist: Die for-Schleife ist schneller, weil mehr Operationen in C und weniger in Python stattfinden .

Im Fall der for-Schleife passieren in C zwei Dinge, die in der while-Schleife in Python behandelt werden:

  1. In der while-Schleife wird der Vergleich i < 100000000in Python ausgeführt, während in der for-Schleife der Job an den Iterator von übergeben wird range(100000000), der intern die Iteration (und damit die Überprüfung der Grenzen) in C durchführt.

  2. In der while-Schleife erfolgt die Aktualisierung der Schleife i += 1in Python, während in der for-Schleife der range(100000000)in C geschriebene Iterator von i+=1(oder ++i) erneut ausgeführt wird.

Wir können sehen, dass es eine Kombination dieser beiden Dinge ist, die die for-Schleife schneller macht, indem sie manuell wieder hinzugefügt werden, um den Unterschied zu erkennen.

import timeit

N = 100000000


def while_loop():
    i = 0
    while i < N:
        i += 1


def for_loop_pure():
    for i in range(N):
        pass


def for_loop_with_increment():
    for i in range(N):
        i += 1


def for_loop_with_test():
    for i in range(N):
        if i < N: pass


def for_loop_with_increment_and_test():
    for i in range(N):
        if i < N: pass
        i += 1


def main():
    print('while loop\t\t', timeit.timeit(while_loop, number=1))
    print('for pure\t\t', timeit.timeit(for_loop_pure, number=1))
    print('for inc\t\t\t', timeit.timeit(for_loop_with_increment, number=1))
    print('for test\t\t', timeit.timeit(for_loop_with_test, number=1))
    print('for inc+test\t', timeit.timeit(for_loop_with_increment_and_test, number=1))


if __name__ == '__main__':
    main()

Ich habe dies sowohl mit der Zahl 100000000 als Literalkonstante als auch mit einer Variablen versucht, Nwie es typischer wäre.

# inline constant N
while loop      3.5131139
for pure        1.3211338000000001
for inc         3.5477727000000003
for test        2.5209639
for inc+test    4.697028999999999

# variable N
while loop      4.1298240999999996
for pure        1.3526357999999998
for inc         3.6060175
for test        3.1093069
for inc+test    5.4753364

Wie Sie sehen können, liegt die whileZeit in beiden Fällen sehr nahe am Unterschied von for inc+testund for pure. Beachten Sie auch, dass in dem Fall, in dem wir die NVariable verwenden, die whileeine zusätzliche Verlangsamung aufweist, um den Wert von wiederholt nachzuschlagen N, die forjedoch nicht.

Es ist wirklich verrückt, dass solche trivialen Änderungen zu einer über 3-fachen Code-Beschleunigung führen können , aber das ist Python für Sie. Und lass mich nicht einmal anfangen, wenn du überhaupt ein eingebautes über eine Schleife verwenden kannst ...

mCoding
quelle