2016-12-26 110 views
12

我讀了很多「避免與numpy循環」。所以,我試了一下。我正在使用這個代碼(簡化版)。一些輔助數據:numpy ufuncs速度vs循環速度

In[1]: import numpy as np 
     resolution = 1000        # this parameter varies 
     tim = np.linspace(-np.pi, np.pi, resolution) 
     prec = np.arange(1, resolution + 1) 
     prec = 2 * prec - 1 
     values = np.zeros_like(tim) 

我的第一個實現與for循環:

In[2]: for i, ti in enumerate(tim): 
      values[i] = np.sum(np.sin(prec * ti)) 

然後,我擺脫了明確for週期,取得了這一點:

In[3]: values = np.sum(np.sin(tim[:, np.newaxis] * prec), axis=1) 

這解決方案對於小陣列來說速度更快,但是當我放大時,我得到了這樣的時間依賴性: enter image description here

我失蹤了還是正常行爲?如果不是,在哪裏挖?

編輯:根據評論,這裏是一些額外的信息。使用IPython的%timeit%%timeit測量時間,每次運行都在新鮮的內核上執行。我的筆記本電腦是acer aspire v7-482pg(i7,8GB)。我使用的是:

  • 蟒蛇3.5.2
  • numpy的1.11.2 + MKL
  • 的Windows 10
+0

真的,我建立的方波,但不污染問題的係數,我簡化了例子。 – godaygo

+2

你有多少內存?如果它不夠大,'tim [:, np.newaxis] * prec'可能需要交換空間,這會導致性能下降。 – unutbu

+0

你如何對兩個功能進行基準測試? –

回答

7

這是正常的預期行爲。這太簡單了,不適用「避免與numpy循環」聲明everywere。如果你正在處理內部循環,它幾乎總是如此。但是在外部循環的情況下(比如你的情況)有更多的例外。特別是如果替代方案是使用廣播,因爲這通過使用很多內存加速您的操作。

只是爲了一點背景知識添加到「避免與numpy的循環」聲明:

NumPy的數組存儲爲連續陣列,類型。 Python int與C int不一樣!因此,無論何時迭代數組中的每個項目,都需要從數組中插入項目,將其轉換爲Python int,然後執行任何您想要的操作,最後您可能需要再次將其轉換爲ac整數(稱爲裝箱和拆箱的價值)。例如,你想用Python sum在數組中的項目:

import numpy as np 
arr = np.arange(1000) 
%%timeit 
acc = 0 
for item in arr: 
    acc += item 
# 1000 loops, best of 3: 478 µs per loop 

您更好地使用numpy的:

%timeit np.sum(arr) 
# 10000 loops, best of 3: 24.2 µs per loop 

即使你將循環推Python的C代碼你遠離numpy表現:

%timeit sum(arr) 
# 1000 loops, best of 3: 387 µs per loop 

從這條規則可能會有例外,但這些將是非常稀疏。至少只要有一些等效的numpy功能。所以如果你想遍歷單個元素,那麼你應該使用numpy。


有時一個普通的python循環就足夠了。這不是廣告宣傳,但與Python功能相比,numpy功能有很大的開銷。例如考慮一個3元素陣列:

arr = np.arange(3) 
%timeit np.sum(arr) 
%timeit sum(arr) 

哪一個會更快?

解決方案:Python的功能性能比numpy的更好的解決方案:

# 10000 loops, best of 3: 21.9 µs per loop <- numpy 
# 100000 loops, best of 3: 6.27 µs per loop <- python 

但是這是什麼都與你的榜樣呢?事實上並非如此,因爲您總是在陣列上使用numpy函數(而不是單個元素,甚至幾個元素),所以內部循環已經使用了優化函數。這就是爲什麼兩者執行大致相同(+/-約10倍,只有很少的元素在約500個元素的因子2)。但這不是真正的循環開銷,而是函數調用開銷!

你的循環液

使用line-profilerresolution = 100

def fun_func(tim, prec, values): 
    for i, ti in enumerate(tim): 
     values[i] = np.sum(np.sin(prec * ti)) 
%lprun -f fun_func fun_func(tim, prec, values) 
Line #  Hits   Time Per Hit % Time Line Contents 
============================================================== 
    1           def fun_func(tim, prec, values): 
    2  101   752  7.4  5.7  for i, ti in enumerate(tim): 
    3  100  12449 124.5  94.3   values[i] = np.sum(np.sin(prec * ti)) 

95%的循環中度過的,我甚至循環體分裂成幾個部分來驗證這一點:

def fun_func(tim, prec, values): 
    for i, ti in enumerate(tim): 
     x = prec * ti 
     x = np.sin(x) 
     x = np.sum(x) 
     values[i] = x 
%lprun -f fun_func fun_func(tim, prec, values) 
Line #  Hits   Time Per Hit % Time Line Contents 
============================================================== 
    1           def fun_func(tim, prec, values): 
    2  101   609  6.0  3.5  for i, ti in enumerate(tim): 
    3  100   4521  45.2  26.3   x = prec * ti 
    4  100   4646  46.5  27.0   x = np.sin(x) 
    5  100   6731  67.3  39.1   x = np.sum(x) 
    6  100   714  7.1  4.1   values[i] = x 

消費者的時間是np.multiplynp.sin,np.sum在這裏,你可以輕鬆地CK通過每次呼叫與他們的開銷比較它們的時間:

arr = np.ones(1, float) 
%timeit np.sum(arr) 
# 10000 loops, best of 3: 22.6 µs per loop 

所以只要比你有類似的運行時間計算運行時COMULATIVE函數調用的開銷很小。即使有100個項目,您也非常接近間接費用時間。訣竅是知道他們在哪個時間點保本。隨着1000項調用的開銷仍然顯著:

%lprun -f fun_func fun_func(tim, prec, values) 
Line #  Hits   Time Per Hit % Time Line Contents 
============================================================== 
    1           def fun_func(tim, prec, values): 
    2  1001   5864  5.9  2.4  for i, ti in enumerate(tim): 
    3  1000  42817  42.8  17.2   x = prec * ti 
    4  1000  119327 119.3  48.0   x = np.sin(x) 
    5  1000  73313  73.3  29.5   x = np.sum(x) 
    6  1000   7287  7.3  2.9   values[i] = x 

但隨着resolution = 5000相比,運行時的開銷是相當低:

Line #  Hits   Time Per Hit % Time Line Contents 
============================================================== 
    1           def fun_func(tim, prec, values): 
    2  5001  29412  5.9  0.9  for i, ti in enumerate(tim): 
    3  5000  388827  77.8  11.6   x = prec * ti 
    4  5000  2442460 488.5  73.2   x = np.sin(x) 
    5  5000  441337  88.3  13.2   x = np.sum(x) 
    6  5000  36187  7.2  1.1   values[i] = x 

當你在每個np.sin花500US叫你不要關心20us的開銷了。

一句話可能是:line_profiler可能包含一些額外的每行開銷,也可能是每個函數調用,所以函數調用開銷忽略的點可能會更低!!!

你的廣播解決方案

我開始剖析的第一個解決方案,讓我們做同樣的與第二個解決方案:

def fun_func(tim, prec, values): 
    x = tim[:, np.newaxis] 
    x = x * prec 
    x = np.sin(x) 
    x = np.sum(x, axis=1) 
    return x 

再次使用line_profiler與resolution=100

%lprun -f fun_func fun_func(tim, prec, values) 
Line #  Hits   Time Per Hit % Time Line Contents 
============================================================== 
    1           def fun_func(tim, prec, values): 
    2   1   27  27.0  0.5  x = tim[:, np.newaxis] 
    3   1   638 638.0  12.9  x = x * prec 
    4   1   3963 3963.0  79.9  x = np.sin(x) 
    5   1   326 326.0  6.6  x = np.sum(x, axis=1) 
    6   1   4  4.0  0.1  return x 

這已經大大超過了開銷時間,因此與循環相比,我們的結果快了10倍。

我也做了分析爲resolution=1000

Line #  Hits   Time Per Hit % Time Line Contents 
============================================================== 
    1           def fun_func(tim, prec, values): 
    2   1   28  28.0  0.0  x = tim[:, np.newaxis] 
    3   1  17716 17716.0  14.6  x = x * prec 
    4   1  91174 91174.0  75.3  x = np.sin(x) 
    5   1  12140 12140.0  10.0  x = np.sum(x, axis=1) 
    6   1   10  10.0  0.0  return x 

precision=5000

Line #  Hits   Time Per Hit % Time Line Contents 
============================================================== 
    1           def fun_func(tim, prec, values): 
    2   1   34  34.0  0.0  x = tim[:, np.newaxis] 
    3   1  333685 333685.0  11.1  x = x * prec 
    4   1  2391812 2391812.0 79.6  x = np.sin(x) 
    5   1  280832 280832.0  9.3  x = np.sum(x, axis=1) 
    6   1   14  14.0  0.0  return x 

的1000尺寸仍然較快,但正如我們所看到那裏調用的開銷仍不 - 在環路解決方案中不可用。但對於resolution = 5000中的每個步驟所花費的時間幾乎是相同的(有些是有點慢,別人快,但整體頗爲相似)

的另一個影響是,實際廣播當你做乘法變得顯著。即使有非常聰明的numpy解決方案,它仍然包含一些額外的計算。對於resolution=10000你看到廣播乘法開始佔用更多的「%的時間」相對於循環解決方案:

Line #  Hits   Time Per Hit % Time Line Contents 
============================================================== 
    1           def broadcast_solution(tim, prec, values): 
    2   1   37  37.0  0.0  x = tim[:, np.newaxis] 
    3   1  1783345 1783345.0 13.9  x = x * prec 
    4   1  9879333 9879333.0 77.1  x = np.sin(x) 
    5   1  1153789 1153789.0  9.0  x = np.sum(x, axis=1) 
    6   1   11  11.0  0.0  return x 


Line #  Hits   Time Per Hit % Time Line Contents 
============================================================== 
    8           def loop_solution(tim, prec, values): 
    9  10001  62502  6.2  0.5  for i, ti in enumerate(tim): 
    10  10000  1287698 128.8  10.5   x = prec * ti 
    11  10000  9758633 975.9  79.7   x = np.sin(x) 
    12  10000  1058995 105.9  8.6   x = np.sum(x) 
    13  10000  75760  7.6  0.6   values[i] = x 

但有實際的,除了時間還有一件事花:內存消耗。你的循環解決方案需要O(n)內存,因爲你總是處理n元素。但是,廣播解決​​方案需要O(n*n)內存。如果在循環中使用resolution=20000,您可能必須等待一段時間,但它仍然只需要8bytes/element * 20000 element ~= 160kB,但在廣播中需要~3GB。這忽略了常數因素(如臨時數組又稱中間數組)!假設你走得更遠,你會非常快地耗盡內存!


時間再總結幾點:

  • 如果你在你這樣做是錯誤的一個numpy的陣列做在單品蟒蛇循環。
  • 如果循環遍歷numpy數組的子陣列,請確保每個循環中的函數調用開銷與函數耗用的時間相比可忽略不計。
  • 如果您廣播numpy陣列,請確保您沒有耗盡內存。

但是關於優化最重要的一點仍然是:

  • 只有優化的代碼,如果是太慢了!如果速度太慢,那麼只有在分析代碼後才進行優化。

  • 不要盲目信任簡化語句,並再次從不優化沒有分析。


最終的一個想法:

,要麼需要一個迴路或廣播這樣的功能可使用容易地實現,如果沒有已經在現有的解決方案。

例如,結合了來自在低resolutions廣播解決方案的速度循環溶液中的存儲器效率是這樣的一個numba功能:

from numba import njit 

import math 

@njit 
def numba_solution(tim, prec, values): 
    size = tim.size 
    for i in range(size): 
     ti = tim[i] 
     x = 0 
     for j in range(size): 
      x += math.sin(prec[j] * ti) 
     values[i] = x 

作爲評價numexpr還可以評估指出廣播的計算速度非常快,,而不需要內存O(n*n)

>>> import numexpr 
>>> tim_2d = tim[:, np.newaxis] 
>>> numexpr.evaluate('sum(sin(tim_2d * prec), axis=1)') 
+0

這個比喻似乎並不有用。目前還不清楚類比中的情境機制如何與實際問題的機制相對應,並且容易產生錯誤的印象,並認爲問題是在一次通話中嘗試做大量工作,而不是試圖使用一個巨大的工作集。 – user2357112

+0

@ user2357112感謝您的反饋!將它再次移除會更好嗎?它被認爲是廣播和循環的隱喻,如果廣播可能會導致記憶錯誤或長時間運行,如果低估了尺寸。這篇文章比我想要的要長得多,所以刪除一些不太理想的部分可能是不錯的。 :-) – MSeifert

+1

我會說刪除它,而不是簡單地總結一下,如何通過Python外部循環來分塊解決問題可以減少內存消耗,同時在'resolution'增加(因爲它是一個外部循環)時引入比例較少的開銷。 – user2357112