while 루프를 사용하는 것보다 파이썬에서 range ()를 반복하는 것이 왜 더 빠릅니까?


81

다른 날 파이썬 벤치마킹을하던 중 흥미로운 것을 발견했습니다. 다음은 거의 동일한 작업을 수행하는 두 개의 루프입니다. 루프 1은 실행하는 데 루프 2의 약 두 배가 걸립니다.

루프 1 :

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

루프 2 :

for n in range(0,100000000):
  pass

첫 번째 루프가 왜 그렇게 느린가요? 나는 그것이 사소한 예라는 것을 알고 있지만 그것은 내 관심을 불러 일으켰습니다. 같은 방식으로 변수를 증가시키는 것보다 더 효율적으로 만드는 range () 함수에 특별한 것이 있습니까?

답변:


159

파이썬 바이트 코드의 분해를 참조하면 더 구체적인 아이디어를 얻을 수 있습니다.

while 루프 사용 :

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

루프 본체에는 10 개의 op가 있습니다.

사용 범위 :

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

루프 본문에는 3 개의 작업이 있습니다.

C 코드를 실행하는 시간은 정수기보다 훨씬 짧으며 무시할 수 있습니다.


2
실제로 첫 번째 분해의 루프 본체에는 10 번의 작업이 있습니다 (위치 32에서 9로 점프). 현재 CPython 구현에서 각 바이트 코드의 해석은 CPU에서 비용이 많이 드는 간접 분기 오 예측 (다음 바이트 코드 구현으로의 점프)에서 상당히 높은 확률로 발생합니다. 이것은 CPython의 현재 구현의 결과이며, 무부하 제비, PyPy 및 기타에 의해 구현되는 JIT는 해당 오버 헤드를 잃을 가능성이 높습니다. 그들 중 최고는 또한 엄청난 속도 향상을 위해 유형 전문화를 할 수있을 것입니다.
Ants Aasma

5
"dis"모듈을 사용하십시오. 함수에 코드를 정의한 다음 dis.disco (FUNC .__ code__)를 호출
kcwu

그러면 더 높은 수준에서 while루프가 각 반복에 대해 비교를 수행해야 한다고 말하는 것이 정확 할까요?
davidhood2

35

range()C로 구현되지만 i += 1해석됩니다.

사용하면 많은 xrange()수의 경우 훨씬 더 빠르게 처리 할 수 ​​있습니다. Python 3.0부터는 range()이전과 동일합니다 xrange().


15

while 루프에서 많은 객체 생성 및 파괴가 진행되고 있다고 말해야합니다.

i += 1

와 같다:

i = i + 1

그러나 Python int는 불변이기 때문에 기존 객체를 수정하지 않습니다. 오히려 그것은 새로운 가치를 가진 새로운 물건을 창조합니다. 기본적으로 다음과 같습니다.

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

가비지 수집기는 또한 많은 양의 정리 작업을 수행해야합니다. "객체 생성은 비싸다".


4

인터프리터에서 C로 작성된 코드로 더 자주 실행되기 때문입니다. 즉, i + = 1은 Python에 있으므로 (비교적으로) 느리지 만 range (0, ...)은 하나의 C 호출이며 for 루프는 대부분 C에서도 실행됩니다.


1

대부분의 Python의 내장 메서드 호출은 C 코드로 실행됩니다. 해석해야하는 코드는 훨씬 느립니다. 메모리 효율성과 실행 속도 측면에서 그 차이는 엄청납니다. 파이썬 내부는 극도로 최적화되었으며 이러한 최적화를 활용하는 것이 가장 좋습니다.


0

여기에 대한 대답은 다른 대답이 제안하는 것보다 조금 더 미묘하다고 생각하지만 그 요점은 정확하지만 for 루프는 더 많은 작업이 C에서 발생하고 Python에서는 덜 발생하기 때문에 더 빠릅니다 .

보다 구체적으로, for 루프의 경우 C에서 while 루프에서 Python으로 처리되는 두 가지 일이 발생합니다.

  1. while 루프에서는 비교 i < 100000000가 Python에서 실행되는 반면, for 루프에서는 작업이의 반복자에 전달되어 range(100000000)내부적으로 C에서 반복 (따라서 경계 검사)을 수행합니다.

  2. while 루프에서 루프 업데이트 i += 1는 Python에서 발생하는 반면 for 루프에서는 다시 range(100000000)C로 작성된 의 반복자가 i+=1(또는 ++i)을 수행합니다.

차이를 확인하기 위해 수동으로 다시 추가하여 for 루프를 더 빠르게 만드는 것이이 두 가지의 조합임을 알 수 있습니다.

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

나는 숫자 100000000 리터럴 상수와 N더 일반적인 변수 인 변수 로 이것을 시도했습니다 .

# 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

보시다시피 두 경우 모두 while시간은 for inc+test및 의 차이에 매우 가깝습니다 for pure. 또한 N변수를 사용하는 경우 while의 값을 반복적으로 조회하기 위해 추가 속도가 느려지 N지만 for그렇지 않습니다.

이러한 사소한 수정으로 인해 코드 속도가 3 배 이상 빨라진다 는 사실은 정말 미쳤습니다 .하지만 그것은 Python입니다. 그리고 루프를 통해 빌트인을 사용할 수있을 때 시작하지 마십시오 ....

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.