2014-04-13 87 views
7

一次,看着邁克·穆勒的性能優化教程(我認爲this one)後,一個念頭就開始住在我的腦海:如果性能問題,最大限度地減少指數,E訪問的循環項目。 G。如果您需要訪問x[1]多次在一個循環for x in l - 一個變量分配給x[1]和循環再利用它。For循環項目拆包

現在我有這個合成例子:

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()函數返回相同的結果。實現是不同的:unpacking()拆包物品放入a和循環b; no_unpacking()通過索引獲取值。

對於python27,它示出了:

1.25280499458 
0.946601867676 
換句話說

unpacking()由〜25%優於no_unpacking()

的問題是:

  • 爲什麼通過指數緩慢的事情(即使在這個簡單的例子)跌顯著訪問?

獎金的問題:

  • 我也試過這對pypy - 有這兩個功能上幾乎沒有區別,性能明智的。這是爲什麼?

感謝您的幫助。

回答

22

回答您的問題,我們可以檢查使用dis模塊的兩個函數生成的字節碼:

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 

我都擴大了列表理解,因爲在python3它會更繁瑣的檢查有趣字節碼。代碼是相同的,所以它對我們的目的無關緊要。

的第一個功能字節碼是:

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  

注意循環必須調用BINARY_SUBSCR兩次訪問元組的兩個元素。

第二函數的字節碼:

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 

注意怎麼沒有BINARY_SUBSCR執行。

所以,好像UNPACK_SEQUENCE加一STORE_FAST(這是由拆包添加的額外操作)更快然後做了兩BINARY_SUBSCR。 這是合理的,因爲BINARY_SUBSCR是一個全面的方法調用,而UNPACK_SEQUENCESTORE_FAST是更簡單的操作。

你可以看到區別,即使在簡單的情況:

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 

正如你可以看到遍歷字符串慢3倍左右使用明確的索引。 由於致電BINARY_SUBSCR,這些都是開銷。

關於第二個問題:pypy的JIT能夠分析代碼並生成優化版本,避免索引操作的開銷。 當它意識到訂閱在元組上完成時,它可能會生成不調用元組方法但直接訪問元素的代碼,從而完全刪除操作。

+0

很好的解釋,很高興知道我並不擔心沒有理由。 – alecxe

+0

很好的回答。很徹底。 – fr1tz

+2

真的很好的解釋。但是,只要將'range'改爲'xrange',筆記本的速度就會從4.0 us降低到3.43 us –