Tại sao lặp over range () trong Python lại nhanh hơn sử dụng vòng lặp while?


81

Một ngày nọ, tôi đang thực hiện một số điểm chuẩn của Python và tôi đã bắt gặp một điều thú vị. Dưới đây là hai vòng lặp ít nhiều làm điều tương tự. Vòng lặp 1 mất khoảng thời gian gấp đôi vòng lặp 2 để thực thi.

Vòng 1:

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

Vòng lặp 2:

for n in range(0,100000000):
  pass

Tại sao vòng lặp đầu tiên lại chậm hơn rất nhiều? Tôi biết đó là một ví dụ tầm thường nhưng nó đã kích thích sự quan tâm của tôi. Có điều gì đặc biệt về hàm range () làm cho nó hiệu quả hơn so với việc tăng một biến theo cùng một cách không?

Câu trả lời:


158

xem phần gỡ mã byte python, bạn có thể có ý tưởng cụ thể hơn

sử dụng vòng lặp 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

Phần thân của vòng lặp có 10 op

phạm vi sử dụng:

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

Phần thân của vòng lặp có 3 op

Thời gian chạy mã C ngắn hơn nhiều so với intepretor và có thể bỏ qua.


2
Trên thực tế, phần thân vòng lặp trong lần tháo gỡ đầu tiên có 10 hoạt động (bước nhảy từ vị trí 32 đến 9). Trong triển khai CPython hiện tại, việc giải thích từng kết quả bytecode với xác suất khá cao trong một nhánh gián tiếp tốn kém dự đoán sai trong CPU (bước nhảy sang việc triển khai bytecode tiếp theo). Đây là hệ quả của việc triển khai CPython hiện tại, các JIT được thực hiện bởi chim én không tải, PyPy và những người khác rất có thể sẽ mất chi phí đó. Những người giỏi nhất trong số họ cũng sẽ có thể thực hiện chuyên môn hóa loại cho một thứ tự tăng tốc độ lớn.
Aasma

5
sử dụng mô-đun "dis". Xác định mã của bạn trong một hàm, sau đó gọi dis.disco (func .__ code__)
kcwu

Vậy có chính xác không khi nói rằng ở cấp độ cao hơn, một whilevòng lặp phải thực hiện so sánh trên mỗi lần lặp?
davidhood2

35

range()được thực hiện trong C, trong khi i += 1được thông dịch.

Sử dụng xrange()có thể làm cho nó nhanh hơn đối với số lượng lớn. Bắt đầu với Python 3.0 range()cũng giống như trước đây xrange().


15

Phải nói rằng có rất nhiều việc tạo ra và phá hủy đối tượng đang diễn ra với vòng lặp while.

i += 1

giống như:

i = i + 1

Nhưng vì các int Python là bất biến, nó không sửa đổi đối tượng hiện có; đúng hơn nó tạo ra một đối tượng hoàn toàn mới với một giá trị mới. Về cơ bản là:

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

Người thu gom rác cũng sẽ có một lượng lớn công việc dọn dẹp. "Tạo đối tượng là tốn kém".


4

Bởi vì bạn đang chạy thường xuyên hơn trong mã được viết bằng C trong trình thông dịch. tức là i + = 1 là trong Python, rất chậm (tương đối), trong khi phạm vi (0, ...) là một trong C gọi vòng lặp for cũng sẽ thực thi chủ yếu trong C.


1

Hầu hết các lệnh gọi phương thức được xây dựng trong Python được chạy dưới dạng mã C. Mã phải được giải thích chậm hơn nhiều. Về hiệu quả bộ nhớ và tốc độ thực thi, sự khác biệt là rất lớn. Nội bộ của python đã được tối ưu hóa đến mức tối đa và tốt nhất là bạn nên tận dụng những tối ưu hóa đó.


0

Tôi nghĩ câu trả lời ở đây tinh tế hơn một chút so với các câu trả lời khác đề xuất, mặc dù ý chính của nó là đúng: vòng lặp for nhanh hơn vì nhiều hoạt động xảy ra hơn trong C và ít hơn trong Python .

Cụ thể hơn, trong trường hợp vòng lặp for, hai điều xảy ra trong C mà trong vòng lặp while được xử lý bằng Python:

  1. Trong vòng lặp while, phép so sánh i < 100000000được thực hiện bằng Python, trong khi trong vòng lặp for, công việc được chuyển cho trình lặp của range(100000000), trình lặp thực hiện lặp lại nội bộ (và kiểm tra giới hạn sau đó) trong C.

  2. Trong vòng lặp while, cập nhật vòng lặp i += 1xảy ra bằng Python, trong khi trong vòng lặp for, trình lặp của range(100000000), được viết bằng C, thực hiện i+=1(hoặc ++i).

Chúng ta có thể thấy rằng sự kết hợp của cả hai điều này đã làm cho vòng lặp for nhanh hơn bằng cách thêm lại chúng theo cách thủ công để thấy sự khác biệt.

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

Tôi đã thử điều này với cả số 100000000 một hằng số theo nghĩa đen và với nó là một biến Nsẽ điển hình hơ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

Như bạn có thể thấy, trong cả hai trường hợp, whilethời gian rất gần với sự khác biệt của for inc+testfor pure. Cũng lưu ý rằng trong trường hợp chúng tôi sử dụng Nbiến, biến whilecó thêm một sự chậm lại để tra cứu liên tục giá trị của N, nhưng forkhông.

Thật là điên rồ khi những sửa đổi nhỏ như vậy có thể dẫn đến tốc độ mã gấp 3 lần , nhưng đó là Python dành cho bạn. Và thậm chí đừng giúp tôi bắt đầu khi bạn có thể sử dụng nội trang qua một vòng lặp nào đó ....

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.