2017-09-12 36 views
6

我試圖做一個純python(沒有外部依賴)元素明智的比較兩個序列。我的第一個解決方案是:地圖vs星圖的性能?

list(map(operator.eq, seq1, seq2)) 

後來我發現starmap功能從itertools,這似乎非常相似我。但在最糟糕的情況下,我的電腦速度竟然快了37%。由於這不明顯給我,我測量所需的時間從發電機獲取1元(不知道這種方式是正確的):

from operator import eq 
from itertools import starmap 

seq1 = [1,2,3]*10000 
seq2 = [1,2,3]*10000 
seq2[-1] = 5 

gen1 = map(eq, seq1, seq2)) 
gen2 = starmap(eq, zip(seq1, seq2)) 

%timeit -n1000 -r10 next(gen1) 
%timeit -n1000 -r10 next(gen2) 

271 ns ± 1.26 ns per loop (mean ± std. dev. of 10 runs, 1000 loops each) 
208 ns ± 1.72 ns per loop (mean ± std. dev. of 10 runs, 1000 loops each) 

在檢索元素的第二種解決方案是24%更好的性能。之後,它們都產生list的相同結果。但是,從什麼地方,我們及時獲得額外的13%:

%timeit list(map(eq, seq1, seq2)) 
%timeit list(starmap(eq, zip(seq1, seq2))) 

5.24 ms ± 29.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 
3.34 ms ± 84.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 

我不知道如何在這樣的嵌套代碼分析的深入挖掘?所以我的問題是,爲什麼第一臺發電機在檢索和獲取list函數中額外的13%時速度如此之快?

編輯: 我的第一個目的是爲了執行逐元素的比較,而不是all,所以all功能用list替換。此替換不會影響時間比例。

在Windows 10(64位)的CPython 3.6.2

+1

爲什麼不直接使用'SEQ1 = = seq2'? –

+0

@Błotosmętek謝謝你的糾正!我的第一個意圖是元素比較,而不是'all',這在我的問題中並不明顯:)真的,如果你用'list'而不是'all'來代替'all',那麼次數的順序將是相同的。 – godaygo

+0

什麼是Python版本?這是CPython嗎? – MSeifert

回答

2

有有助於(結合)的幾個因素所觀察到的性能差異:

  • zip重新使用返回的tuple,如果它具有1的引用計數時下一__next__呼叫被製成。
  • map構建傳遞給每一個__next__調用時的「映射功能」一tuple。實際上,它可能不會從頭創建一個新的元組,因爲Python爲未使用的元組維護一個存儲。但在這種情況下,map必須找到合適大小的未使用的元組。
  • starmap檢查迭代器中的下一項是否爲tuple類型,如果是,則只傳遞它。
  • 使用PyObject_Call從C代碼中調用C函數不會創建傳遞給被調用者的新元組。

所以starmapzip將只使用一個元組一遍遍傳遞給operator.eq從而減少了函數調用的開銷極大。另一方面,map將在每次調用operator.eq時創建一個新的元組(或從CPython 3.6開始填充C數組)。所以速度差異實際上只是元組創建開銷。

而是鏈接到的源代碼,我將提供一些用Cython代碼,可以用來驗證這一點:

In [1]: %load_ext cython 

In [2]: %%cython 
    ...: 
    ...: from cpython.ref cimport Py_DECREF 
    ...: 
    ...: cpdef func(zipper): 
    ...:  a = next(zipper) 
    ...:  print('a', a) 
    ...:  Py_DECREF(a) 
    ...:  b = next(zipper) 
    ...:  print('a', a) 

In [3]: func(zip([1, 2], [1, 2])) 
a (1, 1) 
a (2, 2) 

是,tuple s爲不是真的一成不變的,一個簡單的Py_DECREF足以「把「zip變成相信沒有人擁有對返回元組的引用!

至於「元組直通」:

In [4]: %%cython 
    ...: 
    ...: def func_inner(*args): 
    ...:  print(id(args)) 
    ...: 
    ...: def func(*args): 
    ...:  print(id(args)) 
    ...:  func_inner(*args) 

In [5]: func(1, 2) 
1404350461320 
1404350461320 

因此,元組是正確的通過(只是因爲這些被定義爲C函數!)這不會發生純Python函數傳遞參數:

In [6]: def func_inner(*args): 
    ...:  print(id(args)) 
    ...: 
    ...: def func(*args): 
    ...:  print(id(args)) 
    ...:  func_inner(*args) 
    ...: 

In [7]: func(1, 2) 
1404350436488 
1404352833800 

注意,它也被調用函數不是C函數,即使從C函數調用不會發生:

In [8]: %%cython 
    ...: 
    ...: def func_inner_c(*args): 
    ...:  print(id(args)) 
    ...: 
    ...: def func(inner, *args): 
    ...:  print(id(args)) 
    ...:  inner(*args) 
    ...: 

In [9]: def func_inner_py(*args): 
    ...:  print(id(args)) 
    ...: 
    ...: 

In [10]: func(func_inner_py, 1, 2) 
1404350471944 
1404353010184 

In [11]: func(func_inner_c, 1, 2) 
1404344354824 
1404344354824 

所以有很多的「巧合」領導到如此地步,starmapzip比調用map有多個參數時快被調用函數也是C函數...

1

一個區別我可以看到的是如何map檢索來自iterables項目。 mapzip都從每個可傳遞的迭代中創建一個迭代器的元組。現在zip在內部維護一個result tuple,每當下一次被調用時填充,而在另​​一方面,每次下一次調用時填充result tuple,並釋放它。


* 正如指出的MSeifert直到3.5.4 map_next用於分配一個新的Python元組每次。這在3.6中改變,直到5次迭代使用C堆棧,並且使用大於堆的任何東西。相關PR:Issue #27809: map_next() uses fast callAdd _PY_FASTCALL_SMALL_STACK constant |問題:https://bugs.python.org/issue27809

+0

這會假設這是3.6,請注意[3.5.4]中的代碼(https://github.com/python/cpython/blob/v3 .5.4/Python/bltinmodule.c#L1168-L1192)看起來不同。 :) – MSeifert

+0

@ MSeifert我不知道有多少緩慢/快3.5.4實施與3.6.2相比。 –

+0

(由於堆與C-Stack的訪問,應該慢到5次迭代) –