2014-04-13 21 views
7

Một lần, sau khi xem hướng dẫn tối ưu hóa hiệu suất của Mike Muller (tôi nghĩ rằng this one), một ý nghĩ bắt đầu tồn tại trong đầu của tôi: nếu hiệu suất hoạt động, giảm thiểu các mục truy cập trong vòng lặp theo chỉ mục, e. g. nếu bạn cần truy cập x[1] nhiều lần trong một vòng lặp for x in l - gán một biến cho x[1] và sử dụng lại nó trong vòng lặp.Đối với việc mở rộng vòng lặp

Bây giờ tôi có ví dụ tổng hợp này:

import timeit 

SEQUENCE = zip(range(1000), range(1, 1001)) 

def no_unpacking(): 
    return [item[0] + item[1] for item in SEQUENCE] 


def unpacking():  
    return [a + b for a, b in SEQUENCE] 


print timeit.Timer('no_unpacking()', 'from __main__ import no_unpacking').timeit(10000) 
print timeit.Timer('unpacking()', 'from __main__ import unpacking').timeit(10000) 

unpacking()no_unpacking() chức năng trả lại kết quả tương tự. Việc triển khai khác nhau: unpacking() bỏ các mục vào ab trong vòng lặp; no_unpacking() nhận giá trị theo chỉ mục.

Đối python27, nó cho thấy:

1.25280499458 
0.946601867676 

nói cách khác, unpacking() nhanh hơn so với no_unpacking() bởi ~ 25%.

Câu hỏi đặt ra là:

  • tại sao truy cập bởi những chỉ số chậm lại đáng kể (ngay cả trong trường hợp đơn giản này)?

Bonus câu hỏi:

  • Tôi cũng đã thử này trên pypy - hầu như không có sự khác biệt giữa 2 chức năng này, hiệu suất-khôn ngoan. Tại sao vậy?

Cảm ơn bạn đã được trợ giúp.

Trả lời

22

Để trả lời câu hỏi của bạn, chúng tôi có thể kiểm tra bytecode được tạo ra bởi hai chức năng sử dụng dis mô-đun:

In [5]: def no_unpacking(): 
    ...:  s = [] 
    ...:  for item in SEQUENCE: 
    ...:   s.append(item[0] + item[1]) 
    ...:  return s 
    ...: 
    ...: 
    ...: def unpacking(): 
    ...:  s = [] 
    ...:  for a,b in SEQUENCE: 
    ...:   s.append(a+b) 
    ...:  return s 

tôi đã mở rộng danh sách-hiểu vì trong python3 nó sẽ là cồng kềnh hơn để kiểm tra thú vị bytecodes. Mã tương đương nên nó không thực sự quan trọng cho mục đích của chúng tôi.

Các bytecode cho chức năng đầu tiên là:

In [6]: dis.dis(no_unpacking) 
    2   0 BUILD_LIST    0 
       3 STORE_FAST    0 (s) 

    3   6 SETUP_LOOP    39 (to 48) 
       9 LOAD_GLOBAL    0 (SEQUENCE) 
      12 GET_ITER    
     >> 13 FOR_ITER    31 (to 47) 
      16 STORE_FAST    1 (item) 

    4   19 LOAD_FAST    0 (s) 
      22 LOAD_ATTR    1 (append) 
      25 LOAD_FAST    1 (item) 
      28 LOAD_CONST    1 (0) 
      31 BINARY_SUBSCR   
      32 LOAD_FAST    1 (item) 
      35 LOAD_CONST    2 (1) 
      38 BINARY_SUBSCR   
      39 BINARY_ADD   
      40 CALL_FUNCTION   1 (1 positional, 0 keyword pair) 
      43 POP_TOP    
      44 JUMP_ABSOLUTE   13 
     >> 47 POP_BLOCK    

    5  >> 48 LOAD_FAST    0 (s) 
      51 RETURN_VALUE  

Lưu ý rằng vòng lặp phải gọi BINARY_SUBSCR hai lần để truy cập vào hai yếu tố của tuple.

Các bytecode cho chức năng thứ hai là:

In [7]: dis.dis(unpacking) 
    9   0 BUILD_LIST    0 
       3 STORE_FAST    0 (s) 

10   6 SETUP_LOOP    37 (to 46) 
       9 LOAD_GLOBAL    0 (SEQUENCE) 
      12 GET_ITER    
     >> 13 FOR_ITER    29 (to 45) 
      16 UNPACK_SEQUENCE   2 
      19 STORE_FAST    1 (a) 
      22 STORE_FAST    2 (b) 

11   25 LOAD_FAST    0 (s) 
      28 LOAD_ATTR    1 (append) 
      31 LOAD_FAST    1 (a) 
      34 LOAD_FAST    2 (b) 
      37 BINARY_ADD   
      38 CALL_FUNCTION   1 (1 positional, 0 keyword pair) 
      41 POP_TOP    
      42 JUMP_ABSOLUTE   13 
     >> 45 POP_BLOCK    

12  >> 46 LOAD_FAST    0 (s) 
      49 RETURN_VALUE 

Note thế nào không có BINARY_SUBSCR để thực hiện.

Vì vậy, nó có vẻ như UNPACK_SEQUENCE cộng với một STORE_FAST (đó là các hoạt động bổ sung thêm bằng cách giải nén) được nhanh sau đó làm hai BINARY_SUBSCR. Điều này là hợp lý vì BINARY_SUBSCR là cuộc gọi phương thức đầy đủ, trong khi UNPACK_SEQUENCESTORE_FAST là các thao tác đơn giản hơn.

Bạn có thể thấy sự khác biệt ngay cả trong trường hợp đơn giản:

In [1]: def iter_with_index(s): 
    ...:  for i in range(len(s)): 
    ...:   s[i] 
    ...:   

In [2]: def iter_without_index(s): 
    ...:  for el in s:el 
    ...:  

In [3]: %%timeit s = 'a' * 10000 
    ...: iter_with_index(s) 
    ...: 
1000 loops, best of 3: 583 us per loop 

In [4]: %%timeit s = 'a' * 10000 
    ...: iter_without_index(s) 
    ...: 
1000 loops, best of 3: 206 us per loop 

Như bạn thấy iterating trên một chuỗi là chậm hơn khoảng 3 lần sử dụng chỉ mục rõ ràng. Đó là tất cả chi phí do cuộc gọi đến BINARY_SUBSCR.

Về câu hỏi thứ hai của bạn: pypy có JIT có khả năng phân tích mã và tạo phiên bản được tối ưu hóa để tránh chi phí hoạt động lập chỉ mục. Khi nó nhận ra rằng đăng ký được thực hiện trên bộ dữ liệu, nó có thể tạo mã không gọi phương thức tuple nhưng truy cập trực tiếp vào các phần tử, do đó loại bỏ hoàn toàn các hoạt động BINARY_SUBSCR.

+0

Giải thích tuyệt vời, tốt để biết rằng tôi không quan tâm vì không có lý do gì. – alecxe

+0

Câu trả lời hay. Rất kỹ lưỡng. – fr1tz

+2

Giải thích thực sự thú vị. Tuy nhiên, chỉ bằng cách thay đổi 'dải ô' thành' xrange', tốc độ trên sổ ghi chép của tôi đang thay đổi từ 4.0 xuống còn 3.43 –

Các vấn đề liên quan