簡體   English   中英

Python列表預先設置時間復雜度

[英]Python list prepend time complexity

為什么這個代碼

res = []
for i in range(10000):
    res[0:0] = [i]

大約比這段代碼快十倍?

res = []
for i in range(10000):
    res = [i] + res

我預計兩者都必須移動所有現有的列表元素以將新整數放在零索引處。 當范圍改變時,兩者確實看起來確實是O(n ^ 2),但是切片分配比添加更快,這意味着后者的基本操作大約是10倍。

(是的,兩者都無法實現此結果,並且更好地使用dequeappend然后反轉結果)

你是對的,在高層次上,循環以相同的方式計算基本相同的結果。 因此,時序差異是由於正在使用的Python版本的實現細節。 沒有語言屬性可以解釋差異。

在python.org C實現(CPython)中,代碼實際上在底層有很大的不同。

res[0:0] = [i]

做它看起來像它做什么;-) res的整個內容右移一個槽, i插入左端創建的孔。 大部分時間是通過對平台C庫的memmove()函數的單次調用來消耗的,該函數在一次質量吞吐中進行移位。 現代硬件和C庫非常適合快速移動連續的內存片段(在C級別,是一個Python列表對象)。

res = [i] + res

還有更多內容,主要是由於CPython的引用計數。 它更像是:

create a brand new list object
stuff `i` into it
for each element of `res`, which is a pointer to an int object:
    copy the pointer into the new list object
    dereference the pointer to load the int object's refcount
    increment the refcount
    store the new refcount back into the int object
bind the name `res` to the new list object
decrement the refcount on the old `res` object
at which point the old res's refcount becomes 0 so it's trash
so for each object in the old res:
    dereference the pointer to load the int object's refcount
    decrement the refcount
    store the new refcount back into the int object
    check to see whether the new refcount is zero
    take the "no, it isn't zero" branch
release the memory for the old list object

更多的原始工作,所有指針解除引用都可以跨越內存,這不是緩存友好的。

實施

res[0:0] = [i]

大部分都是這樣的:它從一開始就知道僅僅移動res內容的位置不能對移位對象的refcounts進行任何凈更改,因此不必費心增加或減少任何這些refcounts。 C級memmove()幾乎就是蠟的整個球,並且沒有任何指向int對象的指針需要被取消引用。 不僅原始工作少,而且非常緩存友好。

在每個示例的相關行上運行反匯編,我們得到以下字節碼:

res[0:0] = [i]

  4          25 LOAD_FAST                1 (i)
             28 BUILD_LIST               1
             31 LOAD_FAST                0 (res)
             34 LOAD_CONST               2 (0)
             37 LOAD_CONST               2 (0)
             40 BUILD_SLICE              2
             43 STORE_SUBSCR

res = [i] + res

  4          25 LOAD_FAST                1 (i)
             28 BUILD_LIST               1
             31 LOAD_FAST                0 (res)
             34 BINARY_ADD
             35 STORE_FAST               0 (res)

在第一個例子(切片)中沒有完成BINARY_ADD ,只進行了存儲操作,並且在添加的情況下不僅存儲操作,還有一個BINARY_ADD操作,它做了很多,這可能是為什么它慢得多。 雖然切片表示法確實需要構建切片,但這些操作也非常簡單。

為了更公平的比較,我們可以通過查找替換切片符號,如果它是預構造和存儲的(使用類似s = slice(0, 0) ); 結果字節碼如下所示:

res[s] = [i]

  4          25 LOAD_FAST                1 (i)
             28 BUILD_LIST               1
             31 LOAD_FAST                0 (res)
             34 LOAD_GLOBAL              1 (s)
             37 STORE_SUBSCR

這使得它具有相同數量的字節碼指令計數,現在我們只看到加載和存儲指令,而具有+操作的指令有效地需要額外的指令。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM