python的numpy向量化語句為什麼會比for快?

霍華德:對於 Python 的科學計算有哪些提高運算速度的技巧?

numpy在矩陣乘法運算的時候也一定是按照定義一個個矩陣元素乘起來相加的,為什麼會比幾個for循環快呢?


簡短版:numpy是C寫的。

展開版:為了方便初學者,如python之類語言在背後做了很多很多工作,這才能對外提供一個「傻瓜」界面。

換句話說,「傻瓜」界面並非免費得來,在初學者看不到的地方,隱藏著巨大的、全方位的、不可避免的「消費陷阱」。

我們先來看看,python之類語言的for循環,和其它語言相比,額外付出了什麼。

我們知道,python是解釋執行的。

舉例來說,執行 x = 1234+5678 ,對編譯型語言,是從內存讀入兩個short int到寄存器,然後讀入加法指令,通知CPU內部的加法器動作,最後把加法器輸出存儲到x對應的內存單元(實質上,最後這個動作幾乎總會被自動優化為「把加法器輸出暫存到寄存器而不是內存單元,因為訪問內存的時間消耗常常是訪問寄存器的幾十倍」)。一共2~4條指令(視不同CPU指令集而定)。

而換了解釋性語言,情況就大大不同了。

它得先把「x = 1234+5678」當成字元串,逐個字元比對以分析語法結構——不計空格這也是11個字元,至少要做11個循環;每個循環至少需要執行的指令有:取數據(如讀"x"這個字元)、比較數據、根據比較結果跳轉(可能還得跳轉回來)、累加循環計數器、檢查循環計數器是否到達終值、根據比較結果跳轉。這就是至少6條指令,其中包含一次內存讀取、至少兩次分支指令(現代CPU有分支預測,若命中無額外消耗,否則……)。總計66條指令,比編譯型語言慢至少17倍(假設每條指令執行時間相同。但事實上,訪存/跳轉類指令消耗的時間常常是加法指令的十倍甚至百倍)。

這還只是讀入源碼的消耗,尚未計入「語法分析」這個大頭;加上後,起碼指令數多數百倍(消耗時間嘛……我猜起碼得多數千倍吧)。

不過,python比起其它解釋性語言還是強很多的。因為它可以事先把文本代碼編譯成「位元組碼」(存儲於擴展名為pyc的文件里),從而直接處理整型的「指令代碼」,不再需要從頭開始分析文本。

但是,從「位元組碼」翻譯到實際CPU代碼這步,仍然是省不下的。

這個消耗,可看作「利用虛擬機」執行異構CPU上的程序。有人證明過,哪怕優化到極致,這也需要10倍的性能消耗。

這個消耗也有辦法縮減。這就是JIT技術。

Just-in-time compilation

JIT說白了,就是在第一遍執行一段代碼前,先執行編譯動作,然後執行編譯後的代碼。

如果代碼中沒有循環,那麼這將白白付出很多額外的時間代價;但若有一定規模以上的循環,就可能節省一點時間。

這裡面的佼佼者是Java。它甚至能根據上次運行結果實時profile,然後花大力氣優化關鍵代碼,從而得到比C更快的執行速度。

不過,理想很豐滿,現實很骨感。雖然局部熱點的確可能更快,但Java的整體效率仍然比C/C++差上很多——這個原因就比較複雜了。

和C/C++/Java那種投入海量資源經過千錘百鍊的編譯器不同,python的JIT甚至可稱得上「蹩腳」。

加加減減,僅一個循環,慢上十幾甚至幾十倍還是很正常的。

以上討論,僅僅考慮了for循環這個控制結構本身。事實上,「慢」往往是全方位的。

舉例來說,要計算一組向量,首先就要存儲它。

怎麼存儲呢?

對C/C++來說,就存在「數組」里;而它的數組,就是赤裸裸的一片連續內存區域;區域中每若干個位元組就存儲了一個數值數據。

這種結構CPU處理起來最為方便快捷,且cache友好(若cache不友好就可能慢數倍甚至數十倍)。

Java等其它語言就要稍遜一籌。因為它的「數組」是「真正的數組」;相對於「連續內存區域」,「真正的數組」就不得不在每次訪問時檢查數組下標有無越界。這個檢查開銷不大,但也不小……

當然,這也是有好處的。至少不用像C/C++那樣,整天擔心緩衝區溢出了。

而python之類……

為了遷就初學者,它去掉了「變數聲明」以及「數據類型」——於是它的用戶再也用不著、也沒法寫 int xxx了。隨便什麼數據,咱想存就存,烏拉!

但是,如果我告訴你,可變數據類型其實在C/C++裡面是這樣聲明的呢:

typedef struct tagVARIANT {
union {
struct __tagVARIANT {
VARTYPE vt;
WORD wReserved1;
WORD wReserved2;
WORD wReserved3;
union {
LONGLONG llVal;
LONG lVal;
BYTE bVal;
SHORT iVal;
FLOAT fltVal;
DOUBLE dblVal;
VARIANT_BOOL boolVal;
_VARIANT_BOOL bool;
SCODE scode;
CY cyVal;
DATE date;
BSTR bstrVal;
IUnknown *punkVal;
IDispatch *pdispVal;
SAFEARRAY *parray;
BYTE *pbVal;
SHORT *piVal;
LONG *plVal;
LONGLONG *pllVal;
FLOAT *pfltVal;
DOUBLE *pdblVal;
VARIANT_BOOL *pboolVal;
_VARIANT_BOOL *pbool;
SCODE *pscode;
CY *pcyVal;
DATE *pdate;
BSTR *pbstrVal;
IUnknown **ppunkVal;
IDispatch **ppdispVal;
SAFEARRAY **pparray;
VARIANT *pvarVal;
PVOID byref;
CHAR cVal;
USHORT uiVal;
ULONG ulVal;
ULONGLONG ullVal;
INT intVal;
UINT uintVal;
DECIMAL *pdecVal;
CHAR *pcVal;
USHORT *puiVal;
ULONG *pulVal;
ULONGLONG *pullVal;
INT *pintVal;
UINT *puintVal;
struct __tagBRECORD {
PVOID pvRecord;
IRecordInfo *pRecInfo;
} __VARIANT_NAME_4;
} __VARIANT_NAME_3;
} __VARIANT_NAME_2;
DECIMAL decVal;
} __VARIANT_NAME_1;
} VARIANT, *LPVARIANT, VARIANTARG, *LPVARIANTARG;

VARIANT structure

Variant type - Wikipedia

簡單說,這玩意兒的思路就是「利用一個tag指示數據類型,真正的數據存儲在下面的union里;訪問時,依據tag指示轉換/返回合適類型」。

很顯然,對C/C++/Java程序員來說,這玩意兒無論時間還是空間上,都是個災難。

並且,它也極度的cache不友好——本來可以連續存儲的,現在……變成了個結構體;而且一旦存了某些類型的數據,就不得不通過指針跳轉到另一塊區域才能訪問(如果原地存儲,浪費的空間就太恐怖了)。

所以你看,咱要基於這種結構談效率,是不是有點……

哪怕僅僅了解到這個程度也已經很是觸目驚心了:解釋執行+位元組碼優化慢上至少10倍到幾十上百倍,「初學者友好」的基礎數據又慢上幾倍到幾十倍,透過容器訪問(而非性能更好的、固定大小數組乃至不檢查下標假裝自己是數組的「內存區域」)再慢上幾倍到幾十倍……哪怕咱暫時不考慮其它機制帶來的開銷,僅把這幾樣往一塊一湊(在某些特定的情況下,這些不同的「慢」點還可能相互影響、起到「遲緩度倍增放大」的效果)……

除此之外,還有python內部如何管理/索引/訪問腳本中的全局/局部變數的問題(一般會用dict)、用戶數據和物理機存儲器嚴重不匹配引起的緩存未命中問題、python內部狀態機/執行現場管理等等方面管理的問題——對編譯型語言,這些統統不存在,CPU/內存自己就把自己照顧的很好了;但對解釋性語言,這些都會成為「遲緩度倍增」的元兇。

這些東西的相互影響極為複雜微妙,幾乎沒人能徹底搞明白它。

現在明白,為什麼只要換上C寫的numpy,速度直接就飆高2萬倍、換上cpython又飆高到20萬倍了吧(僅僅是python與numpy的交互都把整個處理拖慢了18萬倍!)?

數據來自題主給的這個鏈接: 霍華德:對於 Python 的科學計算有哪些提高運算速度的技巧?

你看,明白了前因後果,咱是不是只能說「python的優化實在不錯,才僅僅慢了20萬倍而已」呢?(笑~

當然,如果不做這類較為複雜的處理,僅僅是一些流程性的東西的話,這類語言的處理速度還是夠用的——至少與之交互的人感受不到絲毫延遲。

甚至,哪怕需要複雜的處理,這類語言也可以向其它語言求救啊。就好像有個numpy,誰敢說python做不了向量運算呢?

——當然,和行家說話時,你得明白,這是找C之類語言搬救兵了。睜眼說瞎話把它當成python語言自己的能力是有點丟人的。不過如果只混python的圈子的話,這倒也不耽誤什麼。

————————————————————————————

如果要揭短,專業程序員還會把無數據類型導致介面模糊所以無法寫較為複雜的程序之類弊端給你列出一火車的。但這些就是沒必要的題外話了。

畢竟,python只是個膠水語言,初學者友好並且應付常見的簡單應用場景綽綽有餘,這已經足夠了。

就好像把office做的傻瓜化,本就是專業程序員的工作一樣——用戶覺得好用、樂意掏錢就行了,何必關心「做出一套office需要砸進去的錢足夠蓋N座迪拜塔」呢。

當然,如果想進一步發展的話,請記住「在合適的地方用合適的工具」這句話——然後想辦法搞明白每種工具的局限性吧。

畢竟,哪怕是C/C++,在做矩陣之類運算時,也還會求助於SIMD的MMX指令、超線程/多核心CPU乃至GPU,以便為自己「增補」上並行處理能力呢。


向量化有天然並行的語義,可以利用 CPU 的 SIMD 加速指令,比 C 的 for 循環快也正常。除非做一些類似循環展開的優化手段,tirival 的 for 循環也挺傷流水線的,反觀科學計算的庫可以封裝各種妖艷的小優化。


問題前提錯誤,無論是你自己寫矩陣乘法還是用python的庫,除非保證矩陣極其小,否則顯然不可能按照定義一個個元素乘完加起來,就像我們算數的乘法必然是按照位數求出每一位乘法然後加起來而不可能真去循環a次每次sum+=b一樣。

也就是說,雖然你以正常的智商做了基準測試然後大於閾值時用分治法來做矩陣乘法寫了幾百行代碼,但是python的庫顯然也是這麼做的,這種情況下還可以用一些比較Hack的方法去通過犧牲可讀性來提高效率,因此用庫來計算會比你的程序稍微快一點點,比如時間可能只需要你自己寫的代碼的一半。


謝邀,前幾天就看到邀請了,一直沒空寫。今天先來挖個坑。

大家可以去參觀一下numpy.dot 的內核函數:

* This module provides a BLAS optimized matrix multiply, * inner product and dot for numpy arrays:cblas內核函數

你會發現np.dot就是分類討論式的使用blas函數,先判斷好你輸入的數據類型,是標量,一維向量,還是矩陣,然後再調用相應的blas函數。

所以問題python的numpy向量化語句為什麼會比for快?就可以轉化為為什麼blas庫比for快?

為什麼呢?先挖坑,慢慢來答


是用了並行計算嗎


Numpy是用c語言寫的,矩陣運算這麼大的計算量,更追求效率的,比起py肯定要快得多,而且numpy本身就有優化,矩陣乘法不是簡單的循環


推薦閱讀:

能否通過修圖等手段將小解析度的圖片轉變為大解析度?
USB Type-C 如何決定充電方向?
一個不加班的程序員有前途嗎?
次世代遊戲對於PC主機的要求越來越高,遊戲實際上對於主機的要求真的很高嗎?
為什麼計算機和一些電子產品的時間選擇在1970年?

TAG:Python | 計算機 | 科學計算 |