高效能的Python擴充套件(2)

Janzou發表於2014-11-05

簡介

這篇文章是這系列文章的第二篇,我們的關注點在使用Numpy API為Python編寫C擴充套件模組的過程。在第一部分中,我們建立了一個簡單的N體模擬,並發現其瓶頸是計算體之間的相互作用力,這是一個複雜度為O(N^2)的操作。通過在C語言中實現一個時間演化函式,我們大概能以大約70倍來加速計算。

如果你還沒有看過第一篇文章,你應該在繼續看這篇文章之前先看一下

在這篇文章中,我們將犧牲我們程式碼中的一些通用性來提升效能。

回顧

Wrold是儲存N體狀態的一個類。我們的模擬將演化一系列時間步長下的狀態。

在開始模擬時,N體被隨機分配質量m,位置r和速度v。對於每個時間步長,接下來的計算有:

1. 合力F,每個體上的合力根據所有其他體的計算。
2. 速度v,由於力的作用每個體的速度被改變。
3. 位置R,由於速度每個體的位置被改變。

訪問巨集

我們在第一部分建立的擴充套件模組使用巨集來獲取C語言中NumPy陣列的元素。下面是這些巨集中的一些巨集的形式:

像這樣使用巨集,我們能使用像r(i, j)這樣的簡單標記來訪問py_r陣列中的元素。不管陣列已經以某種形式被重塑或切片,索引值將匹配你在Python中看到的形式。

對於通用的程式碼,這就是我們想要的。在我們模擬的情況下,我們知道我們的陣列的特點:它們是連續的,並且從未被切片或重塑。我們可以利用這一點來簡化和提升我們程式碼的效能。

簡單的C擴充套件 2

在本節中,我們將看到一個修改版本的C擴充套件,它擯棄了訪問巨集和NumPy陣列底層資料的直接操作。本節中的程式碼src/simple2.c可在github上獲得。

為了進行比較,之前的實現也可在這裡獲得。

演化函式

從檔案的底部開始,我們可以看到,evolve函式與之前的版本一樣,以相同的方式解析Python引數,但現在我們利用PyArray_DATA巨集來獲得一個紙箱底層的記憶體。我們將這個指標命名為npy_float64,作為double的一個別名。

從我們以前使用巨集的實現來看,唯一的變化是我們必須明確地計算陣列索引。我們可以將底層陣列描述為二維npy_float64矩陣,但這需要一定的前期成本和記憶體管理。

計算力

與之前的實現一樣,力以相同的方式進行計算。唯一的不同在符號,以及在for-loops迴圈中顯式索引的計算。

效能

我很驚訝地發現,相比於我們原來的C擴充套件,現在這種實現效能提升了45%。在相同的i5桌上型電腦上,以前的版本每秒執行17972個時間步長,而現在每秒執行26108個時間步長。這代表101倍提升了原始Python和NumPy的實現。

 

使用SIMD指令

在上面的實現中,我們在x和y方向上明確地計算出向量分量。如果我們願意放棄一些可移植性,並希望加快速度,我們可以使用x86的SIMD(單指令多資料)指令來簡化我們的程式碼。

本節中的程式碼src/simd1.c,可在github上獲得。

x86內部

在下面的程式碼中,我們將利用向量資料型別__m128d。數字128代表向量中的位元組數,而字母“d”表示向量分量的資料型別。在這種情況下,分量的型別是double(64位元組浮點)。其他向量的寬度和型別可根據不同的架構獲得。

多種內部函式都可以使用向量資料型別。英特爾在其網站上提供了一個方便的參考。

可移植性

我的工作環境是使用GNU gcc編譯器的Debian Wheezy系統。在這種環境下,定義可用的內部資料型別和函式的標頭檔案是x86intrin.h。這可能不能跨平臺移植。

演化函式

這裡的evolve函式比之前的版本更加緊湊。二維陣列轉換為__mm128d指標,並利用向量來排除顯式計算向量分量的需要。

計算力

這裡力的計算也比之前的實現更加緊湊。

效能

雖然這個明確的向量化程式碼更清晰,並比以前的版本更加緊湊​​,它的執行速度只提升了約1%,它每秒執行26427個時間步長。這可能是因為編譯器能夠使用相同的向量指令優化之前的執行情況。

結論

如果全通用性是沒有必要的,經常的效能提升,可以通過直接用C語言訪問NumPy陣列的底層記憶體來實現。

而在現在這個例項中,顯式使用向量內部沒有提供多少好處,相對效能差異將在使用OpenMP的多芯設定中增大。

下一部分

在本系列文章的下一部分,我們將利用OpenMP來並行使用這裡的實現,以及看如何利用多核心來擴充套件效能。

如果您有任何疑問,意見,建議或更正,請通過聯絡連結告訴我。

相關文章