簡體   English   中英

使用for循環vs列表理解來優化代碼

[英]Using a for loop vs list comprehension to optimise code

為什么與使用for循環而不是使用列表理解的most_all_b相比,most_all_a效率更高? 他們肯定都是O(n)嗎? 編輯:在另一個問題中的答案是非特定的,基本上說一個取決於情況,可以比另一個更快。 那么,這種情況呢?

def almost_all_a(numbers):
    sumall = sum(numbers)
    rangelen = range(len(numbers))
    return [sumall - numbers[i] for i in rangelen]

def almost_all_b(numbers):
    sumall = sum(numbers)
    for i in range(len(numbers)):
        numbers[i] = sumall - numbers[i]

有關問題的答案包含了所有內容,但以某種方式被隱藏了。

讓我們看一看almost_all_a是什么:它建立一個與原始列表大小相同的新列表,然后返回該新列表。 對於大列表,這將使用列表所需內存的兩倍(假設此處為數字列表)。 並以這種方式調用該函數: nums = almost_all_a(nums) ,您只是在構建一個新列表,完成nums = almost_all_a(nums)丟棄前一個列表。 對性能的兩個影響:需要(臨時)內存,並且需要垃圾收集器清理舊列表。

almost_all_b中,什么都沒有發生:您只是在更改列表元素:沒有額外的分配(內存增益),也沒有任何東西可以收集(執行時間增益)。

TL / DR:是什么讓a版本丟失,重要的是它歸結為分配一個新的列表,而相關的回答說:

由於創建和擴展列表的開銷很大,因此使用列表推導代替不構建列表的循環,無意義地累積無意義的值列表然后將其丟棄的方法通常會較慢。

您的復雜性分析是正確的: n操作來計算總和加上n操作來計算在這兩種情況下的列表使得O(n)

但是,在我們談論速度之前,您肯定已經注意到, almost_all_b具有副作用,而almost_all_a沒有副作用。 更糟糕的是, almost_all_b不是冪等的。 如果您重復調用almost_all_b ,則每次都會修改參數numbers 除非您有很好的理由,否則您應該更喜歡almost_all_aalmost_all_b因為它更易於理解並且不易出錯

基准1

我將嘗試用timeit確認您的斷言( almost_all_aalmost_all_a [比] almost_all_b有效 ):

>>> from timeit import timeit
>>> ns=list(range(100))
>>> timeit(lambda: almost_all_a(ns), number=10000)
0.06381335399782984
>>> timeit(lambda: almost_all_b(ns), number=10000)
2.3228586789991823

哇! almost_all_a比快約35倍almost_all_b 不,那是個玩笑。 您可以看到發生了什么: almost_all_b [1,...,90]進行了almost_all_b 10000次的almost_all_b ,所以該數字瘋狂地增加了:

>>> len(str(ns[0])) # number of digits of the first element!
19959

好的,那只是說服您避免具有副作用的功能。

基准2

現在,真正的測試:

>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_a})
5.720672591000039
>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_b})
5.937547881

請注意,基准測試可能會在另一個列表或另一個平台上給出不同的結果。 (考慮一下,如果列表占用了90%的可用RAM,將會發生什么。)但是讓我們假設我們可以一概而論。

Python字節碼

讓我們看一下字節碼:

>>> import dis
>>> dis.dis(almost_all_a)
  2           0 LOAD_GLOBAL              0 (sum)
              2 LOAD_DEREF               0 (numbers)
              4 CALL_FUNCTION            1
              6 STORE_DEREF              1 (sumall)

  3           8 LOAD_GLOBAL              1 (range)
             10 LOAD_GLOBAL              2 (len)
             12 LOAD_DEREF               0 (numbers)
             14 CALL_FUNCTION            1
             16 CALL_FUNCTION            1
             18 STORE_FAST               1 (rangelen)

  4          20 LOAD_CLOSURE             0 (numbers)
             22 LOAD_CLOSURE             1 (sumall)
             24 BUILD_TUPLE              2
             26 LOAD_CONST               1 (<code object <listcomp> at 0x7fdc551dee40, file "<stdin>", line 4>)
             28 LOAD_CONST               2 ('almost_all_a.<locals>.<listcomp>')
             30 MAKE_FUNCTION            8
             32 LOAD_FAST                1 (rangelen)
             34 GET_ITER
             36 CALL_FUNCTION            1
             38 RETURN_VALUE

和:

>>> dis.dis(almost_all_b)
  2           0 LOAD_GLOBAL              0 (sum)
              2 LOAD_FAST                0 (numbers)
              4 CALL_FUNCTION            1
              6 STORE_FAST               1 (sumall)

  3           8 SETUP_LOOP              36 (to 46)
             10 LOAD_GLOBAL              1 (range)
             12 LOAD_GLOBAL              2 (len)
             14 LOAD_FAST                0 (numbers)
             16 CALL_FUNCTION            1
             18 CALL_FUNCTION            1
             20 GET_ITER
        >>   22 FOR_ITER                20 (to 44)
             24 STORE_FAST               2 (i)

  4          26 LOAD_FAST                1 (sumall)
             28 LOAD_FAST                0 (numbers)
             30 LOAD_FAST                2 (i)
             32 BINARY_SUBSCR
             34 BINARY_SUBTRACT
             36 LOAD_FAST                0 (numbers)
             38 LOAD_FAST                2 (i)
             40 STORE_SUBSCR
             42 JUMP_ABSOLUTE           22
        >>   44 POP_BLOCK
        >>   46 LOAD_CONST               0 (None)
             48 RETURN_VALUE

開始幾乎是一樣的。 然后,您將獲得一個類似於黑匣子的列表理解功能。 如果打開盒子,我們將看到:

>>> dis.dis(almost_all_a.__code__.co_consts[1])
  4           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                16 (to 22)
              6 STORE_FAST               1 (i)
              8 LOAD_DEREF               1 (sumall)
             10 LOAD_DEREF               0 (numbers)
             12 LOAD_FAST                1 (i)
             14 BINARY_SUBSCR
             16 BINARY_SUBTRACT
             18 LIST_APPEND              2
             20 JUMP_ABSOLUTE            4
        >>   22 RETURN_VALUE

您有兩個區別:

  • 在列表理解中, sumallnumbers都加載了LOAD_DEREF而不是LOAD_FAST (這對於閉包是正常的),並且應該慢一些;
  • 在列表理解中, LIST_APPEND替換對numbers[i] LOAD_FAST(numbers)/LOAD_FAST(i)/STORE_SUBSCR第36-40行的LOAD_FAST(numbers)/LOAD_FAST(i)/STORE_SUBSCR )。

我的猜測是開銷來自該分配。

另一個基准

您可以重寫almost_all_a ,使其更整潔,因為您不需要索引:

def almost_all_c(numbers):
    sumall = sum(numbers)
    return [sumall - n for n in numbers]

這個版本(在我的示例+平台上)比almost_all_aalmost_all_a

>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_a})
5.755438814000172
>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_b})
5.93645353099987
>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_c})
4.571863283000084

(請注意,在Python中,更整潔的版本是最快的版本。) almost_all_aalmost_all_c之間的區別是使用對第inumbers項的訪問(您可以反編譯要檢查的almost_all_c )。

Conlusion

我似乎這里的瓶頸是訪問第inumbers

  • 一次在almost_all_a ;
  • almost_all_balmost_all_b
  • 永遠不會在almost_all_c

這就是為什么almost_all_a快於almost_all_b的原因。

暫無
暫無

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

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