2017-05-26 30 views
1

在一些優化,我在Python 2.7的代碼工作,我偶然發現了以下現象:通過預先分配和將`my_list [x] = y`填充爲`__setitem__`的語法糖來填充python列表。

>>> from timeit import timeit 
>>> def fill_by_appending(): 
...  my_list = [] 
...  for x in xrange(1000000): 
...    my_list.append(x) 
...  return my_list 
... 
>>> def fill_preallocated_1(): 
...  my_list = [0]*1000000 
...  for x in xrange(1000000): 
...    my_list[x] = x 
...  return my_list 
... 
>>> def fill_preallocated_2(): 
...  my_list = [0]*1000000 
...  for x in xrange(1000000): 
...    my_list.__setitem__(x,x) 
...  return my_list 
... 
>>> def fill_by_comprehension(): 
...  my_list = [x for x in xrange(1000000)] 
...  return my_list 
... 
>>> assert fill_by_appending() == fill_preallocated_1() == fill_preallocated_2() == fill_by_comprehension() 
>>> timeit("fill_by_appending()", setup="from __main__ import fill_by_appending", number=100) 
5.877948999404907 
>>> timeit("fill_preallocated_1()", setup="from __main__ import fill_preallocated_1", number=100) 
3.964423894882202 
>>> timeit("fill_preallocated_2()", setup="from __main__ import fill_preallocated_2", number=100) 
12.38241720199585 
>>> timeit("fill_by_comprehension()", setup="from __main__ import fill_by_comprehension", number=100) 
2.742932081222534 

,這並不奇怪,我認爲預分配是不是追加更快,或者說理解是比什麼都更快,但爲什麼使用__setitem__比使用[]慢三倍

起初,我有一個理論,使用my_list[x] = x或者只是重新分配存儲在my_list[x]新對象的地址的引用,或者也許是,解釋甚至注意到,兩人都是同一類型的,並使用一個重載的賦值運算符,而setitem調用實際複製內存在,但有些實驗證明我錯了:

>>> class MyList(list): 
...  def __setitem__(self, x,y): 
...    super(MyList, self).__setitem__(x,y**2) 
... 
>>> ml = MyList([1,2,3,4,5]) 
>>> ml[2]=10 
>>> ml 
[1, 2, 100, 4, 5] 

是否有人知道什麼是引擎蓋下回事?

+1

我猜'__setitem__'是一個函數調用,當'[X] ='有解釋特殊的意義。爲什麼'[]'快於'list()'來創建新列表的原因相同。 – Igonato

+0

注意:對於這個特定的情況,真正的答案是「只要調用'範圍(1000000)'」(或者在Py3上,'list(range(1000000))'如果你確實需要'list')假設你正在考慮稍微複雜的填充模式。 – ShadowRanger

+0

@ShadowRanger略微:) – gmoss

回答

2

通用方法分派擁有基於語法派遣額外的開銷;後者必須反覆查找並創建一個綁定方法,並通過通用方法調度機制調用該調用(更一般的==較慢)。通用調度還意味着構造要傳遞的參數的tuple(其中基於語法的調用只是從Python堆棧讀取值而不構建tuple)。

此外,Python級別名稱實際上是一個薄包裝,所以調用__setitem__意味着在到達C API之前調用一個額外的層,因爲它必須在到達sq_ass_item之前遍歷一個附加層(C層插槽這是實現任務的最終呼叫)。 METH_COEXIST can be used to limit the slot wrapper overhead根據文檔,但它看起來像that was only used for __getitem__ on list

您可以通過存儲綁定方法來消除方法查找和綁定開銷,這可能會節省一些工作量,但對於CPython而言,基本上,語法會節省方法調用;如果語法同樣清晰且不易出錯,請使用語法。可能會降低一些差異的預綁定的例子是:

def fill_preallocated_3(): 
    my_list = [0]*1000000 
    set = my_list.__setitem__ 
    for x in xrange(1000000): 
     set(x,x) 
    return my_list 
+0

謝謝,尤其是對於CPython源碼的鏈接!你知道爲什麼選擇把'__setitem__'放在額外層後面嗎?它是否與繼承有關? – gmoss

+1

@gmoss:不。他們有一個通用包裝器,用於每個定義的C級插槽; C級插槽的參數已被解析,因此C - > C調用不需要打包只能立即解壓縮的參數;包裝器提供了從Python級通用調用約定到槽特定調用原型的轉換層。這是通用「可變參數」函數調度與特定C語言函數原型的成本的一部分。 – ShadowRanger

+1

爲了記錄,不一直需要執行'METH_COEXIST'的原因是它並不是經常需要的('__setitem__'不是經常直接調用的;它通常繞過直接通過語法調度路徑進入C級插槽) ,唯一的辦法是使它比解析並傳遞給C插槽的包裝器更快地實現它,它是在共存的Python暴露函數中重現C級函數的大部分代碼(基本上是複製粘貼大量代碼) 。不值得維護的麻煩獲得相當小的收益。 – ShadowRanger

1

你有額外的屬性查詢+第二函數的函數調用:

def fill_preallocated_1():

  ... 
     32 LOAD_FAST    1 (x) 
     35 LOAD_FAST    0 (my_list) 
     38 LOAD_FAST    1 (x) 
     41 STORE_SUBSCR   
     ... 

def fill_preallocated_2():

  ... 
     32 LOAD_FAST    0 (my_list) 
     35 LOAD_ATTR    1 (__setitem__) 
     38 LOAD_FAST    1 (x) 
     41 LOAD_FAST    1 (x) 
     44 CALL_FUNCTION   2 
     ... 
+0

注意:屬性查找開銷可能是泛型'CALL_FUNCTION'與特定'STORE_SUBSCR'的開銷的一小部分。 [我的回答](https://stackoverflow.com/a/44191990/364696)包含一個變體測試,可以消除查找資產的花費,幫助隔離減速的組件。 – ShadowRanger

+2

函數調用是主要的罪魁禍首 - 緩存'__setitem__'屬性在我的系統上提高了大約30%。沒有東西可以扔掉,但優化的列表分配速度要快三倍以上。順便說一句。在我的系統上,相同的測試:'fill_preallocated_1:4.73s; fill_preallocated_2:13.41s; fill_preallocated_3:10.8s' – zwer

+0

感謝您的確認! – ShadowRanger