C++中使用SIMD

圖7列出了使用SIMD技術的多種方法,我們先按從上至下的順序簡要介紹每一種,然後重點介紹彙編語言方法。

圖1  使用SIMD技術的多種方法

圖7 使用SIMD技術的多種方法

 

第一種方法是使用著名的IPP庫,IPP的全稱是Intel Integrated Performance Primitives, 是英特爾公司開發的一套跨平臺軟件函數庫,提供了非常廣泛的功能,包括各種常用的圖形圖像、音視頻處理函數。因爲其中的很多函數都已經使用SIMD技術做了優化,所以使用這個庫是使用SIMD技術的一個快捷途徑。通過鏈接https://software.intel.com/en-us/intel-ipp/ 可以訪問IPP的官方介紹,瞭解更多信息。

第二種方法是使用編譯器的自動向量化(Auto-vectorization)支持。比如圖8是在Visual Studio(C++)中通過項目屬性對話框啓用自動向量化的截圖。

 

圖8 在Visual Studio中啓用自動向量化支持

 

經筆者分析,這樣啓用後編譯好的程序中確實使用了一些SIMD指令,比如圖9右側藍色加亮那一行使用的便是SSE2中的cvtsi2sd指令,它可以將源操作數中的有符號雙字整數轉換成目標操作數中的雙精度浮點值。

 

圖9 觀察編譯器自動向量化產生的SIMD指令

 

如果使用GCC編譯器,那麼可以使用類似這樣的命令行來編譯: 
代碼1 
如果希望看到編譯器所採取的向量化動作,那麼可以增加-ftree-vectorizer-verbose=1,於是可以類似圖10的輸出信息。

 

圖10 使用GCC的自動向量化支持

 

使用GDB的反彙編功能,可以很容易地觀察到GCC產生的SIMD指令,如圖11所示。

 

圖11 GCC的自動向量化功能產生的彙編指令

 

第三種方法是使用編譯器指示符(compiler directive),比如,如果使用英特爾的C/C++編譯器(ICC)編譯如下代碼,那麼ICC便會對#pragma simd指示符下面的for循環做向量化,並給出類似下面這樣的輸出信息:remark: SIMD LOOP WAS VECTORIZED.

代碼2

第四種方法是使用Cilk技術。Cilk一詞源於發音相近的Silk一詞,蘊含的意思是要把並行編程做的像絲綢一樣美麗。Cilk技術最早由MIT開發,第一版本於1994年發佈。後來開發者創建了一個名叫 Cilk Arts的公司,推出改進的私有版本。2009年,英特爾收購了Cilk Arts,將Cilk技術整合進英特爾編譯器中。2012年後,Cilk再次成爲開源項目,GCC中便有支持(需要4.8或者更高版本)。感興趣的朋友可以從https://www.cilkplus.org網站了解更多信息和下載有關工具及示例代碼。

第五種方法是使用編譯器的內建函數(intrinsic),舉例來說,下面這個循環來自我們要詳細討論的圖像二值化程序的C++代碼。

代碼3

如果使用Visual C++編譯器的SIMD intrinsic進行改寫,那麼新的代碼如清單1所示。

清單1 通過intrinsic使用SIMD技術

代碼4

第六種方法是直接使用匯編語言編寫彙編函數,然後再從C++代碼中調用匯編函數,稍後會詳細介紹。

比較圖7中的六種方法,靈活度和可控性由上至下越來越高,但是使用的難度基本也是越來越大。 
圖1  使用SIMD技術的多種方法

編寫和調試供SIMD彙編函數

有兩種方法可以在C++項目中使用匯編代碼,一種是通過__asm{}這樣的指示符號把彙編代碼嵌入在C++函數中,另一種是把彙編代碼放在單獨的以.asm結尾的文件中。前一種方法因爲不支持64位,所以基本過時了。

在使用後一種方法時,首先要在項目的Solution Explorer樹形控件上右擊希望加入彙編文件的項目,然後選擇Build Dependencies → Build Customizations調出圖6所示的對話框,然後選中masm行。 

詳細討論如何編寫彙編代碼超出了本文的範圍,這裏只能管中窺豹,介紹與上面討論的for循環(清單1上方)對應的一段彙編指令(引自《現代x86彙編語言程序設計》一書),如清單2所示。

清單2 對灰度圖像進行二值化處理的SSE2彙編程序片段

代碼5

對於長久沒有寫過悉彙編代碼的同行,理解清單2中的代碼可能有些困難,特別是其中的SIMD指令。下面將以筆者慣用的調試方法來幫助大家理解——在調試器裏看SIMD。

在清單2的第一行指令處設置斷點(與在C++代碼中設置斷點方法相同),觸發程序調用這個彙編函數,斷點命中後,單步走過這條指令。

代碼6

簡單說,這條指令就是把edx指向的ITD結構體的Threshold字段賦給EAX寄存器。

打開彙編窗口,編譯後的指令爲:

代碼7

這意味着,Threshold字段在結構體中的偏移是0xC。Ctrl + Alt + A調出Visual Studio的命令窗口,觀察內存和寄存器的值,可以印證:

代碼8

看來,這條指令的作用就是把二值化的閾值加載到EAX寄存器。

接下來一條指令比較簡單,movd xmm1,eax,就是把常規寄存器EAX中存放的閾值傳遞給SSE的SSE寄存器XMM1。Visual Studio的寄存器窗口默認不顯示SIMD的寄存器,但是可以通過快捷菜單很容易解決這個問題,點擊右鍵,調出圖12所示的快捷菜單,選中SSE即可。

 

圖12 配置顯示SSE寄存器

 

選中SSE後,單步執行,再觀察寄存器窗口,可以看到XMM1的值如下:

代碼9

接下來的兩條指令是要把已經在XMM1最低字節中的閾值(0x98)散列(shuffle)到其他字節中。

代碼10

兩條指令中的p代表packed,即組合數,是SIMD中的常見術語,pshufb是Packed Shuffle Bytes的縮寫,它根據第二個操作數指定的控制掩碼對第一個操作數執行散列操作,產生一個組合數。描述起來比較拗口,單步執行這兩條指令後看一下效果大家就明白了:

代碼11

有趣吧。接下來的這條指令(movdqa xmm2,xmmword ptr [PixelScale])是把PixelScale常量數組中的縮放值賦給XMM2。

代碼12

執行後,XMM2的值爲:

代碼13

接下來的指令是做組合減法,最有SIMD特色的操作。

代碼14

單步前兩個寄存器的值爲:

代碼15

單步後爲:

代碼16

也就是一次完成16個整數減法。

做好準備工作後,接下來就開始處理ESI指向的圖像數據了,movdqa xmm0,[esi]每次可以把16個字節加載到XMM0,psubb xmm0,xmm2減去縮放值(128),然後使用下面這條pcmpgtb指令進行比較。

代碼17

pcmpgtb的全稱是Compare packed signed byte integers for greater than,它會根據比較結果來把目標字節寫爲全0或者全1(大於)。例如,單步前的XMM0和XMM1如果是:

代碼18

那麼單步後便是:

代碼19

而後,movdqa [edi],xmm0指令把結果寫到EDI指向的目標緩衝區中。然後把ESI和EDI都增加16,進行下一次循環。

圖13是比較編寫的測試程序界面,列表控件中包含了以不同方式對同一幅圖像執行二值化操作測量到的時間。

 

圖13 比較不同計算方式的測試程序

 

從圖13可以看到,與普通的C++代碼(圖中以ALU表示)相比,使用SSE方法的速度提升是非常明顯的,從原來的1000多毫秒,加快到了10/20多毫秒,這就是SIMD的魅力。篇幅關係,要就此打住了,感興趣的朋友可以下載示例程序的完整源代碼(http://advdbg.org/的資源板塊)親自體驗一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章