GCC中使用SIMD

©2004本文最初由IBM developerWorks中國網站發表,其網址是http://www-900.ibm.com/developerworks/cn,文章鏈接爲這裏。

Abstract:
X86架構上的多媒體應用開發,如果能夠使用SIMD指令進行優化, 性能將大大提高。目前,IA-32的SIMD指令包括MMX,SSE,SSE2等幾級。 在GCC的開發環境中,有幾種使用SIMD指令的方式,本文逐一介紹。
Keywords: 優化,GCC, SIMD,MMX,SSE

 

 

 
IA-32 Intel體系結構的指令主要分爲以下幾類 [1]:
通用
x87 FPU
MMX技術
SSE/SSE2/SSE3擴展
MMX/SSE類擴展引入了SIMD(單指令多數據)的執行模式,可用於加速多媒體應用。 下面簡要介紹一下這些指令的執行環境和特徵。

 

8個32位通用寄存器可爲各個SIMD擴展所使用;
MMX:8個64位MMX寄存器(mm0 - mm7),也可爲各SSE擴展所使用;
數據爲整數,最多支持兩個32位
運算中沒有寄存器能夠進行溢出指示
SSE:8個128位xmm寄存器,MXSCR寄存器,EFLAGS寄存器
支持單精度浮點
MXSCR含有rounding, overflow標誌
支持64位SIMD整數
SSE2:執行環境同sse
雙精度浮點
128位整數
雙—單精度轉換
SSE3:與Inte Prescott處理器一同發佈不久,共13條指令
主要增強了視頻解碼、3D圖形優化和超線程性能
MMX技術出現最早,目前幾乎所有的X86處理器都提供支持,包括嵌入式X86, 所以下面的討論主要基於MMX,但方法完全適用於SSEn, 包括像AMD的3D Now等其它SIMD擴展。

MMX指令又分爲以下幾種:

數據傳送:movd, movq
數據轉換:packsswb, packssdw, packuswb, punpckhbw, punpckhwd, punpckhdq, punpcklbw, punpcklwd, punpckldq
並行算術:paddb, paddw, paddd, paddsb, paddsw, paddusb, paddusw, psubb, psubw, psubd, psubsb, psubsw, psubusb, psubusb, psubusw, pmulhw, pmullw, pmaddwd
並行比較:pcmpeqb, pcmpeqw, pcmpeqd, pcmpgtb, pcmpgtw, pcmpgtd
並行邏輯:pand, pandn, por, pxor
移位與旋轉:psllw, pslld, psllq, psrlw, psrld, psrlq, psraw, psrad
狀態管理:emms
這些指令除了需要注意功能外,還需要注意處理的數據類型。以上內容爲背景介紹,細節請參考手冊。

 

 
當使用C/C++完成了一個嵌入式應用的所有功能,性能問題常擺在面前, 這時可以使用profile工具(如gprof)找出產生瓶頸的函數, 將這些函數使用匯編徹底重寫, 例如MPEG-4編解碼器xvid項目 [4]就使用了這種方法, 而且針對不同處理器/指令集分別給出了不同的優化, 正是如此該項目無論功能、還是性能均爲一流, 顯然這是深度優化的目標所在。

在使用流水線、VLIW以及SIMD的體系結構(比如某些DSP)上, 整個函數的手工優化可以帶來幾倍到幾十倍的性能提升。 不過,性能允許,對於函數內關鍵部分使用一些特定的實現, 既突出重點提高性能,又可以盡多地利用C/C++的高級特徵, 相對縮短開發週期。 下面給出使用GCC時,應用MMX指令的幾種混合編程方法:

Intel C/C++ 編譯器intrinsics
GCC builtin操作
嵌入彙編asm construct
 

 
查看IA-32 Intel指令集手冊 [2]時, 部分指令的解釋中會有一項“Intel C/C++ Compiler Intrinsic Equivalent”, 會指出該指令對等的intrinsic。 intrinsic在C/C++程序中的語法是以函數形式出現, 編譯時可以直接翻譯爲一條MMX指令(複合情況會生成最直接的幾條), 換言之,如果不使用intrinsic,可能需要多條C/C++語句完成, 而編譯器卻並不能保證將這幾條語句能夠生成這條最高效的MMX指令。 並不是每條MMX指令都有對等的intrinsic, 手冊的附錄中列出了所有的, 它們分爲簡單型(simple)和複合型(composite)兩種, 每個簡單型的就是對應一條指令,而複合型則對應多條指令。
GCC支持Intel C/C++ Compiler Intrinsics。用法如下示例:

      #include <stdio.h>
      #include <xmmintrin.h> /*一定需要包括此頭文件*/

      /*gcc -Wall -march=pentium4 -mmmx -o ins  mmx_ins.c*/

      int main(int argc,char *argv[])
      {
        /*使用MMX做以下向量的點積*/
        short in1[] = {1, 2, 3, 4};
        short in2[] = {2, 3, 4, 5};
        int out1;
        int out2;

        __m64 m1;    /* MMX支持64位整數的mm寄存器 */
        __m64 m2;    /* MMX操作需要使用mm寄存器 */
        __m128 m128; /* for SSEn only*/

        /*每次往mm寄存器裝入兩個short型的數,注意是兩個*/
        m1 = _mm_cvtsi32_si64(((int*)in1)[0]);
        m2 = _mm_cvtsi32_si64(((int*)in2)[0]);

        /*一條指令進行4個16位整數的乘加*/
        /*生成兩個32位整數*/
        m2  = _mm_madd_pi16(m1, m2);

        /*將低32位整數放入通用寄存器*/
        out1 =  _mm_cvtsi64_si32(m2);

        /*將高32位整數右移後,放入通用寄存器*/
        m2  = _mm_slli_pi32(m2, 32);
        out2 =  _mm_cvtsi64_si32(m2);

        /*清除MMX狀態*/
        _mm_empty();

        /*將兩個32位數相加,結果爲8*/
        out1 += out2;
        printf("a: %d/n", out1);

        return(0);
      }
幾點說明:

即使你不是P4平臺,編譯時也請使用以下選項,
        /*gcc -Wall -march=pentium4 -mmmx -o ins  mmx_ins.c*/
否則,會出現如下類似信息:
        ...xmmintrin.h:34:3: #error "SSE instruction set not enabled"
最終結果實際並沒有求得四對乘積的和,只是前兩對的, instrinsic _mm_cvtsi32_si64只向mm寄存器放入了低32位,高32位爲零, 但mmx有指令movq可以做到64位的數據傳送,intrinsic沒有對應, 這也說明並不是所有的指令有等價的intrinsic。
當計算的向量爲兩對0x8000, 0x8000時,即 (-215)*(-215) + (-215)*(-215), 結果應該爲231,但計算出來的值是-231, 因爲發生了溢出,可程序無從知道。 這是使用MMX時,應特別注意的,計算溢出沒有任何標誌位指示,一個極大的值變爲極小,SSE對此做了改善。
程序不再使用MMX之時,注意使用emms指令清除MMX狀態。
 

 
什麼是built-in操作?就是對待MMX操作數,就如int, float等基本數據類型一般, 有相應定義的操作,如加(+)、減(-),或者數據類型之間的轉換。 詳細內容參考GNU GCC Manual [5] Extensions to the C Language Family->Built-in Functions-> X86 Built-in Functions一節。
一些MMX指令有其相應的built-in操作, 下面一段代碼爲例:

      include <stdio.h>
      /*無需特別的頭文件,built-in嘛*/

      /* gcc -Wall  -o bins  builtinmmx.c*/

      /*定義了一個vector數據類型,hi表示16位,4表示4個*/
      typedef int v4hi __attribute__ ((mode(V4HI)));

      /*定義了2個32位的vector類型,si表示32位*/
      typedef int v2si __attribute__ ((mode(V2SI)));

      int main(int argc,char *argv[])
      {
        short pa[4] = {0x8000, 0x8000, 1, -1};
        short pb[4] = {0x8000, 0x7FFF, -1, -2};
       
        v4hi va, vb;
        v4hi vsum;
       
        va = ((v4hi*)pa)[0];
        vb = ((v4hi*)pb)[0];
       
        /* 4個16位進行飽和加 */
        //vsum = __builtin_ia32_paddsw(va, vb);

        /* 4個16位還可以直接進行加法,但不同於兩個long long相加 */
        vsum =  va + vb;
       
        /*vector的輸出還需要強制轉換爲long long*/
        printf("...with MMX instructions...to compute vec_add: %llx /n", (long long)vsum);
       
        //結果1:0xfffd0000ffff8000
        //結果2:0xfffd0000ffff0000
       
        return(0);
      }
幾點說明:

是的,這裏built-in vector及其操作,隨着GCC的發展正在加強。如果需要使用以上範例,應使用GCC 3.4以上版本;
使用builtin函數時,與intrinsic相似;但本質卻是不同,這裏兩個向量使用‘+’操作就說明了vector也如其它數據類型一樣,編譯器直接支持,只不過這裏的加法就是指四個單元數分別相加,低位單元的進位不會影響相鄰高位單元的數據;
vector還可以強制轉換爲通用數據。
 


GCC一開始就允許C代碼中嵌入asm指令,並不只是針對MMX指令, 不過對於MMX技術,顯然也是一個很好的利用方法, 詳細的語法請參考GNU GCC手冊 [5], 或者GCC: The Complete Reference [6]''Inline Assembly''一節。
如下是一個點積的例子:

      #include <stdio.h>

      /** GCC -o ins  inlinemmx.c **/

      int main(int argc,char *argv[])
      {
        int i;
        int result;
        short a[] = {1, 2, 3, 4, 5, 6, 7, 8};
        short b[] = {1, 1, 1, 1, 1, 1, 1, 1};

        printf("...with MMX instructions.../n");
       
        /*首先,將點積合累積寄存器清零,實際缺省就爲0?*/
        asm("pandn %%mm5,%%mm5;"::);

        /*讀入a, b,每四對數相乘後分兩組相加,形成兩組和*/
        /*這裏的循環控制是C在做*/
        for(i = 0; i < sizeof(a)/sizeof(short); i += 4){
          asm("movq %0,%%mm0;/
          movq %1,%%mm1;/
          pmaddwd %%mm1,%%mm0;/
          paddd %%mm0,%%mm5; #相乘後相加 "
          :
          : "m" (a[i]), "m" (b[i]));
        }

        /*將兩組和分離,並相加*/
        asm("movq %%mm5, %%mm0;/
        psrlq $32,%%mm5;/
        paddd %%mm0, %%mm5;/
        movd %%mm5,%0;/
        emms"
        :"=r" (result)
        :);

        printf("result: 0x%x/n", result);
        //這裏結果爲0x24

        return(0);
      }
幾點說明:

這裏是典型的在函數中C和彙編混合編程;
注意彙編指令中操作數的順序;
這裏可以直接使用movq等沒有intrinsics/built-in對應的指令;
注意在asm指令序列中間不要加雜註釋,可能導致生成的代碼不正確。
 

 
下面是合成濾波器(Synthesis Filter)的一個優化過程, 合成濾波器在語音編解碼中有廣泛應用, 運行時也佔用了整個算法中較高比例的時間。
 

      for (i = 0; i < lg; i++)
      {
        s = L_mult(x[i], a[0]);/*L_mult是相乘後左移*/
        for (j = 1; j <= M; j++){/*M這裏固定爲10*/
          s = L_msu(s, a[j], yy[-j]);/*L_msu是乘減後左移操作*/
        }
 
        s = L_shl(s, 3); /*左移三位*/
        *yy++ = g729round(s);
      }
      #endif
上面的代碼,因爲內存循環爲10,可以考慮展開,並統一操作爲乘加指令。

      /*爲了使用乘加操作,需要調整10個係數的順序*/
      for(i = 0; i < M; i++)
        ta[i] = -a[M - i];

      ta[11] = 0;
      ta[10] = a[0];

      for (i = 0; i < lg; i++){
        *yy = x[i];
        yy[1] = 0;

        s = L_mac(s, ta[11], yy[1]);
        s = L_mac(s, ta[10], yy[0]);
        s = L_mac(s, ta[9], yy[-1]);
        s = L_mac(s, ta[8], yy[-2]);
        s = L_mac(s, ta[7], yy[-3]);
        s = L_mac(s, ta[6], yy[-4]);
        s = L_mac(s, ta[5], yy[-5]);
        s = L_mac(s, ta[4], yy[-6]);
        s = L_mac(s, ta[3], yy[-7]);
        s = L_mac(s, ta[2], yy[-8]);
        s = L_mac(s, ta[1], yy[-9]);
        s = L_mac(s, ta[0], yy[-10]);
       
        s = L_shl(s, 3);
        *yy++ = g729round(s);
      }
以上循環內核正好可以將MMX的8個寄存器全部利用。

      /*爲了使用乘加操作,需要調整10個係數的順序*/
      for(i = 0; i < M; i++)
        ta[i] = -a[M - i];

      ta[11] = 0;
      ta[10] = a[0];

      /*11個係數分別放入3個MMX寄存器,0作填充*/
      asm("movq %0,%%mm0;/
      movq %1,%%mm1;/
      movq %2,%%mm2"/
      :/
      : "m" (ta[0]), "m" (ta[4]), "m"(ta[8]));
     
      /*利用MMX技術進行濾波器核心操作*/
      for (i = 0; i < lg; i++){
        *yy = x[i];
        yy[1] = 0;

        asm("pandn %%mm6,%%mm6;/
        movq %1,%%mm3;/
        movq %2,%%mm4;/
        movq %3,%%mm5;/
        pmaddwd %%mm0,%%mm3;/
        pmaddwd %%mm1,%%mm4;/
        pmaddwd %%mm2,%%mm5;/
        paddd %%mm3, %%mm6;/
        paddd %%mm4, %%mm6;/
        paddd %%mm5, %%mm6;/
        movq  %%mm6, %%mm7;/
        psrlq $32, %%mm6;/
        paddd %%mm7, %%mm6;/
        movd %%mm6,%0;/
        emms"
        :
        :"r"(s), "m" (yy[-10]), "m" (yy[-6]), "m"(yy[-2]));

 /*因爲指令結果飽和屬性的限制,s還沒有左移,所以下面多做一位飽和左移*/
        s = L_shl(s, 4);
        *yy++ = g729round(s);
      }
幾點說明:
注意:以上嵌入的彙編代碼輸出結果s放在了輸入處,屬於實踐中的個案;
MMX沒有乘左移之類的DSP指令,甚至還沒有加飽和之類的操作,SSE中有一定增強;
以上操作,理論上存在溢出可能,所以最後使用原有的飽和左移操作,減少了一定風險;
上面的部分代碼操作顯然允許並行,這在VLIW系統中十分有用;
這已經形成了該濾波器全面優化的核心。
 

 
如果願意盡多地利用SIMD技術,可能需要更多地使用匯編級的編碼, 不過也有一些高級語言和彙編的混合編程技術能夠幫助你, 它們有的提高性能更大一些, 有的形式上更優雅些,本質上效率也不錯, 都不失好的方法,建議嘗試。
正是如此,一方面CPU上支持越來越多的SIMD指令集擴展, 另一方面GCC也正在加緊支持這些擴展的易用,對,正在, 碰到一些問題,先想辦法繞過去, 這裏使用GCC 3.4.1,根據經驗效果還是不錯的。

 

 
 

 
Intel: IA-32 Intel Architechture Software Developer's Manual, Volume 1: Basic Architecture(2002)
 

 
Intel: IA-32 Intel Architechture Software Developer's Manual, Volume 2: Instruction Set Reference(2003)
 

 
Intel: IA-32 Intel Architechture Software Developer's Manual, Volume 3: System Programming Guide(2003)
 

 
XviD.org,http://www.xvid.org/(up-to-date)
 

 
GNU, GCC online documentation, http://www.gnu.org/software/GCC/onlinedocs/(up-to-date)
 

 
Authur Griffith, GCC: The Complete Referencea, McGraw Hill(2002)
 

 

 
 GCC中SIMD指令的應用方法
This document was generated using the LaTeX2HTML translator Version 2002 (1.62)

Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney.

The command line arguments were:
latex2html -iso_language CN -html_version 4.0,unicode -address '©2004 CoreUp Designs' -local_icons -split 0 -nonavigation gccsimd

The translation was initiated by on 2004-12-27

 

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