[转]numpy性能优化
轉自:http://blog.csdn.net/pipisorry/article/details/39087583
http://blog.csdn.net/pipisorry/article/details/39087583
Introduction
NumPy提供了一個特殊的數據類型ndarray,其在向量計算上做了優化。這個對象是科學數值計算中大多數算法的核心。
相比于原生的Python,利用NumPy數組可以獲得顯著的性能加速,尤其是當你的計算遵循單指令多數據流(SIMD)范式時。
然而,利用NumPy也有可能有意無意地寫出未優化的代碼。下面這些技巧可以幫助你編寫高效的NumPy代碼。
避免不必要的數據拷貝
查看數組的內存地址
1. 查看靜默數組拷貝的第一步是在內存中找到數組的地址。下邊的函數就是做這個的:
| ? | def?id(x): ????# This function returns the memory block address of an array. ????returnx.__array_interface__['data'][0] |
2. 有時你可能需要復制一個數組,例如你需要在操作一個數組時,內存中仍然保留其原始副本。
| ? | a?=np.zeros(10); aid=id(a); aid 71211328 b?=a.copy();id(b)==aid False |
Note:python列表的淺拷貝和深拷貝-列表拷貝一節
具有相同數據地址(比如id函數的返回值)的兩個數組,共享底層數據緩沖區。然而,共享底層數據緩沖區的數組,只有當它們具有相同的偏移量(意味著它們的第一個元素相同)時,才具有相同的數據地址。共享數據緩沖區,但偏移量不同的兩個數組,在內存地址上有細微的差別:
| ? | id(a),id(a[1:]) (71211328,71211336) |
在這篇文章中,我們將確保函數用到的數組具有相同的偏移量。
下邊是一個判斷兩個數組是否共享相同數據的更可靠的方案:
| ? | def get_data_base(arr): ????"""For a given Numpy array, finds the base array that "owns" the actual data.""" ????base = arr ????whileis instance(base.base, np.ndarray): ????????base = base.base ????return?base def arrays_share_data(x, y): ????return?get_data_base(x)is?get_data_base(y) print(arrays_share_data(a,a.copy()), arrays_share_data(a,a[1:])) False True |
感謝Michael Droettboom指出這種更精確的方法,提出這個替代方案。
Note:a和a[1:]id雖然不同,但是他們是共享內存的。
就地操作和隱式拷貝操作
3. 數組計算包括就地操作(下面第一個例子:數組修改)或隱式拷貝操作(第二個例子:創建一個新的數組)。
| 1 2 3 4 5 | a?*=2;id(a)==aid True c?=a*2;id(c)==aid False |
一定要選擇真正需要的操作類型。隱式拷貝操作很明顯很慢,如下所示:
| 1 2 3 4 5 6 7 | %%timeit a=np.zeros(10000000) a?*=2 10?loops, best of?3:19.2ms per loop %%timeit a=np.zeros(10000000) b?=a*2 10?loops, best of?3:42.6ms per loop |
4.?重塑數組可能涉及到拷貝操作,也可能涉及不到。
例如,重塑一個二維矩陣不涉及拷貝操作,除非它被轉置(或更一般的非連續操作):
| 1 2 | a?=np.zeros((10,10)); aid?=?id(a); aid 53423728 |
重塑一個數組,同時保留其順序,并不觸發拷貝操作。
| 1 2 | b?=a.reshape((1,-1));id(b)==aid True |
轉置一個數組會改變其順序,所以這種重塑會觸發拷貝操作。
| 1 2 | c?=a.T.reshape((1,-1));id(c)==aid False |
因此,后邊的指令比前邊的指令明顯要慢。
5.?數組的flatten和revel方法將數組變為一個一維向量(鋪平數組)。flatten方法總是返回一個拷貝后的副本,而revel方法只有當有必要時才返回一個拷貝后的副本(所以該方法要快得多,尤其是在大數組上進行操作時)。
| 1 2 3 4 5 6 7 8 9 10 11 | d?=a.flatten();id(d)==aid False e?=a.ravel();id(e)==aid True %timeit a.flatten() 1000000?loops, best of?3:881ns per loop %timeit a.ravel() 1000000loops, best of3:294ns per loop |
廣播規則
廣播規則允許你在形狀不同但卻兼容的數組上進行計算。換句話說,你并不總是需要重塑或鋪平數組,使它們的形狀匹配。
廣播規則描述了具有不同維度和/或形狀的數組仍可以用于計算。一般的規則是:當兩個維度相等,或其中一個為1時,它們是兼容的。NumPy使用這個規則,從后邊的維數開始,向前推導,來比較兩個元素級數組的形狀。最小的維度在內部被自動延伸,從而匹配其他維度,但此操作并不涉及任何內存復制。
下面的例子說明了兩個向量之間進行矢量積的兩個方法:第一個方法涉及到數組的變形操作,第二個方法涉及到廣播規則。顯然第二個方法是要快得多。
?
[python]?view plaincopy print? [numpy教程- 通用函數ufunc- 廣播規則]
?
NumPy數組進行高效的選擇
NumPy提供了多種數組分片的方式。
數組視圖涉及到一個數組的原始數據緩沖區,但具有不同的偏移量,形狀和步長。NumPy只允許等步長選擇(即線性分隔索引)。
NumPy還提供沿一個軸進行任意選擇的特定功能。
最后,花式索引(fancy indexing)是最一般的選擇方法,但正如我們將要在文章中看到的那樣,它同時也是最慢的。
1. 創建一個具有很多行的數組。我們將沿第一維選擇該數組的分片。
| ? | n, d?=100000,100 a?=np.random.random_sample((n, d)); aid=id(a) |
數組視圖和花式索引
2. 每10行選擇一行,這里用到了兩個不同的方法(數組視圖和花式索引)。
| ? | b1?=a[::10] b2?=a[np.arange(0, n,10)] np.array_equal(b1, b2) True |
3.?數組視圖指向原始數據緩沖區,而花式索引產生一個拷貝副本。
| ? | id(b1)==aid,id(b2)==aid (True,False) |
兩個方法的執行效率,花式索引慢好幾個數量級,因為它要復制一個大數組。
替代花式索引:索引列表
當需要沿一個維度進行非等步長選擇時,數組視圖就無能為力了。
然而,替代花式索引的方法在這種情況下依然存在。給定一個索引列表,NumPy的函數可以沿一個軸執行選擇操作。
| ? | i?=np.arange(0, n,10) b1?=a[i] b2?=np.take(a, i, axis=0) np.array_equal(b1, b2) True |
第二個方法更快一點:
| 1 2 3 4 5 | %timeit a[i] 100?loops, best of?3:13ms per loop %timeit np.take(a, i, axis=0) 100?loops, best of?3:4.87ms per loop |
替代花式索引:布爾掩碼
當沿一個軸進行選擇的索引是通過一個布爾掩碼向量指定時,compress函數可以作為花式索引的替代方案。
| 1 | i?=np.random.random_sample(n) < .5 |
可以使用花式索引或者np.compress函數進行選擇。
| ? | b1?=a[i] b2?=np.compress(i, a, axis=0) np.array_equal(b1, b2) True %timeit a[i] 10?loops, best of?3:59.8ms per loop %timeit np.compress(i, a, axis=0) 10?loops, best of?3:24.1ms per loop |
第二個方法同樣比花式索引快得多。
花式索引是進行數組任意選擇的最一般方法。然而,往往會存在更有效、更快的方法,應盡可能首選那些方法。
當進行等步長選擇時應該使用數組視圖,但需要注意這樣一個事實:視圖涉及到原始數據緩沖區。
?
?
為什么NumPy數組如此高效?
一個NumPy數組基本上是由元數據(維數、形狀、數據類型等)和實際數據構成。數據存儲在一個均勻連續的內存塊中,該內存在系統內存(隨機存取存儲器,或RAM)的一個特定地址處,被稱為數據緩沖區。這是和list等純Python結構的主要區別,list的元素在系統內存中是分散存儲的。這是使NumPy數組如此高效的決定性因素。
為什么這會如此重要?主要原因是:
1. 低級語言比如C,可以很高效的實現數組計算(NumPy的很大一部分實際上是用C編寫)。例如,知道了內存塊地址和數據類型,數組計算只是簡單遍歷其中所有的元素。但在Python中使用list實現,會有很大的開銷。
2. 內存訪問模式中的空間位置訪問會產生顯著地性能提高,尤其要感謝CPU緩存。事實上,緩存將字節塊從RAM加載到CPU寄存器。然后相鄰元素就能高效地被加載了(順序位置,或引用位置)。
3. 數據元素連續地存儲在內存中,所以NumPy可以利用現代CPU的矢量化指令,像英特爾的SSE和AVX,AMD的XOP等。例如,為了作為CPU指令實現的矢量化算術計算,可以加載在128,256或512位寄存器中的多個連續的浮點數。
4. NumPy可以通過Intel Math Kernel Library (MKL)與高度優化的線性代數庫相連,比如BLAS和LAPACK。NumPy中一些特定的矩陣計算也可能是多線程,充分利用了現代多核處理器的優勢。
總之,將數據存儲在一個連續的內存塊中,根據內存訪問模式,CPU緩存和矢量化指令,可以確保以最佳方式使用現代CPU的體系結構。
就地操作和隱式拷貝操作之間的區別
讓我們解釋一下技巧3。類似于a *= 2這樣的表達式對應一個就地操作,即數組的所有元素值被乘以2。相比之下,a = a*2意味著創建了一個包含a*2結果值的新數組,變量a此時指向這個新數組。舊數組變為了無引用的,將被垃圾回收器刪除。第一種情況中沒有發生內存分配,相反,第二種情況中發生了內存分配。
更一般的情況,類似于a[i:j]這樣的表達式是數組某些部分的視圖:它們指向包含數據的內存緩沖區。利用就地操作改變它們,會改變原始數據。因此,a[:] = a * 2的結果是一個就地操作,和a = a * 2不一樣。
知道NumPy的這種細節可以幫助你解決一些錯誤(例如數組因為在一個視圖上的一個操作,被無意中修改),并能通過減少不必要的副本數量,優化代碼的速度和內存消耗。
為什么有些數組不進行拷貝操作,就不能被重塑?
一個轉置的二維矩陣不依靠拷貝就無法進行鋪平。一個二維矩陣包含的元素通過兩個數字(行和列)進行索引,但它在內部是作為一個一維連續內存塊存儲的,可使用一個數字訪問。
有多個在一維內存塊中存儲矩陣元素的方法:我們可以先放第一行的元素,然后第二行,以此類推,或者先放第一列的元素,然后第二列,以此類推。第一種方法叫做行優先排序,而后一種方法稱為列優先排序。這兩種方法之間的選擇只是一個內部約定問題:NumPy使用行優先排序,類似于C,而不同于FORTRAN。
更一般的情況,NumPy使用步長的概念進行多維索引和元素的底層序列(一維)內存位置之間的轉換。array[i1, i2]和內部數據的相關字節地址之間的具體映射關系為:
| 1 | offset = array.strides[0] * i1 + array.strides[1] * i2 |
重塑一個數組時,NumPy會盡可能通過修改步長屬性來避免拷貝。例如,當轉置一個矩陣時,步長的順序被翻轉,但底層數據仍然是相同的。然而,僅簡單地依靠修改步長無法完成鋪平一個轉置數組的操作(嘗試下!),所以需要一個副本。
Recipe 4.6(NumPy中使用步長技巧)包含步長方面更廣泛的討論。同時,Recipe4.7(使用步長技巧實現一個高效的移動平均算法)展示了如何使用步伐加快特定數組計算。
from:http://blog.csdn.net/pipisorry/article/details/39087583
ref:Getting the Best Performance out of NumPy
IPython Cookbook
如何寫出比 MATLAB 更快的矩陣運算程序?
Numpy使用MKL庫提升計算性能
轉載于:https://www.cnblogs.com/McKean/p/6221053.html
總結
以上是生活随笔為你收集整理的[转]numpy性能优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: yum安装Imagick及扩展
- 下一篇: C# 去除文件和文件夹的只读属性