sse實戰1 - 入門與性能對比

入門推薦閱讀該URL裏面的系列文章,http://www.cnblogs.com/zyl910/archive/2012/04/26/md00.html。
MSDN上有全部可用的函數及說明,而且非常容易懂,非常推薦,http://msdn.microsoft.com/en-US/library/k0ya3x0e(v=vs.80).aspx,注意左邊的分類,已經把全部可用的函數整理得非常條理啦。
 

簡單來講,SSE提高性能的方法就是通過一條指令處理多條數據,與OPENMP或OPENCV等講究核間的並行不同,MMX、SSE等SIMD指令集提升的是核內的並行度。現代CPU都是多發射長流水線,使用增強指令集可以充分填充流水線從而使其滿負荷運轉。
剛開始推薦使用MS的VC2005及以上IDE,一是有許多可視化工具提升效率,而且MS提供的intrinsic系列函數提供了較好的封裝,不需要使用匯編和操作SSE相關寄存器,很大程度上降低了開發成本和風險,入門後再轉用GCC等也比較容易。
我使用的開發平臺是VS2012,X245 CPU,最高支持到SSE3。從05後就可以直接include <intrin.h>來使用所有的intrinsic函數。
現在拿一段主要是矩陣減法的代碼爲例,解釋如何使用SSE以及相關性能對比,測試代碼及SSE版的代碼如下:
static inline void* align(const void* x, int n)
{
  int p = (int)x;
  p = (p % n == 0)? (p) : (p + n - p % n);
  return (void*)p;
}

static inline float compare2(const std::vector<int>& a_vec, const std::vector<int>& b_vec)
{
  static const int n_maxoffset = 80;
  const int32_t* a = &a_vec[0];
  const int32_t* b = &b_vec[0];
  int asize = a_vec.size();
  int bsize = b_vec.size();
  
  char a_block_head[MATCH_MASK * 2 + 16u] = {0};
  uint16_t* aoffsets = (uint16_t*)align(a_block_head, 16);
  char b_block_head[MATCH_MASK * 2 + 16u] = {0};
  uint16_t* boffsets = (uint16_t*)align(b_block_head, 16);

  auto fn_init_offsets = [](const int32_t* x, int n_size, uint16_t offsets[])->void
  {
    for (int i = 0; i < n_size; ++i)
      offsets[MATCH_STRIP(x[i])] = i;
  };
  fn_init_offsets(a, asize, aoffsets);
  fn_init_offsets(b, bsize, boffsets);
  uint8_t topcount = 0;
  int topoffset = 0;
  {
#if 0
    for (int i = 0; i < MATCH_MASK; ++i)
    {
      if (aoffsets[i] && boffsets[i])
      {
        aoffsets[i] -= boffsets[i];
      }
    }
  }
#else
    __m128i* p_aoffsets = (__m128i*)aoffsets;
    __m128i* p_boffsets = (__m128i*)boffsets;
    int n_round = MATCH_MASK / 8;
    for(int i = 0; i < n_round; ++i)
    {
      p_aoffsets[i] = _mm_subs_epu16(p_aoffsets[i], p_boffsets[i]);
    }
    for (int i = n_round * 8; i < MATCH_MASK; ++i)
    {
      aoffsets[i] -= boffsets[i];
    }
  }
#endif
  return aoffsets[2];
}
 
對比的部分在#if 0及#else相關部分,#if 0中的爲普通版本,#else爲SSE版本。
這裏我們使用的是__m128i即128位緊縮整數,類似的類型還有__m64i,__m256i,分別對應MMX及AVX指令集。我的古老X245並不支持AVX,如果有支持AVX指令集的CPU可以依後面所講的方法嘗試使用__m256i進行優化,一般來講性能會成倍增加。
首先,進行SSE運算的內存一定要內存對齊,否則會運行出錯!所以最開始使用了align,具體原因可參考上一篇VS中使用SSE DEBUG正常RELEASE下出錯的解決方法
接下來就是把要計算的數組打包爲__m128i,這裏的數組每個元素爲16位整數即每8個元素打包爲一個__m128i。我們將指針類型轉換爲__m128i然後通過n_round次循環處理完前面的能被16整除的塊,剩餘的不足16個元素再單獨處理。
這裏採用的intrisic函數爲_mm_subs_epu16,作用是將傳入的兩個__m128i作爲16位無符號整型的數組對應位置作飽合減法後返回__m128i作爲結果。飽合減法是一個新概念,即在計算溢出時取邊界值而非像普通運算那樣運算完後捨去高位。如對16位無符號整型,0xffff + 1普通加法結果爲0,即捨去進位後溢出的高位而只取低位的0,但對飽合加法則會取邊界的0xffff作爲結果。四則運算都有對應的飽合版本,可以跳轉到emmintrin.h查看類似的函數,飽合版本的函數名會有額外的's',如_mm_subs_epu16對比_mm_sub_epu16,字母'u'和'i'則區分函數的符號和無符號版本,如_mm_sub_epi16爲有符號版本。
接下來我們嘗試測試兩個版本的性能差異,每40000個循環作爲一輪取時長,一共跑15輪取其平均值:
int nRound = 0;
  int nTotalTime = 0;
  while (nRound < 15)
  {
    int nStart = ::GetTickCount();
    float f_score = 0;
    for (int i = 0; i < 40000; ++i)
      f_score = compare2(src_array, dst_array);
    int nEnd = ::GetTickCount() - nStart;
    nTotalTime += nEnd;
    ++nRound;
    printf("nRound: %d, nAvgMs: %d, f_score: %f, nEnd: %d\n", nRound, nTotalTime / nRound, f_score, nEnd);
  }
分別#if 0和#if 1得到兩遍結果如下:
普通版本
nRound: 1, nAvgMs: 1139, f_score: 120.000000, nEnd: 1139
nRound: 2, nAvgMs: 1131, f_score: 120.000000, nEnd: 1123
nRound: 3, nAvgMs: 1123, f_score: 120.000000, nEnd: 1107
nRound: 4, nAvgMs: 1119, f_score: 120.000000, nEnd: 1108
nRound: 5, nAvgMs: 1120, f_score: 120.000000, nEnd: 1123
nRound: 6, nAvgMs: 1118, f_score: 120.000000, nEnd: 1108
nRound: 7, nAvgMs: 1116, f_score: 120.000000, nEnd: 1107
nRound: 8, nAvgMs: 1115, f_score: 120.000000, nEnd: 1108
nRound: 9, nAvgMs: 1116, f_score: 120.000000, nEnd: 1123
nRound: 10, nAvgMs: 1115, f_score: 120.000000, nEnd: 1108
nRound: 11, nAvgMs: 1114, f_score: 120.000000, nEnd: 1107
nRound: 12, nAvgMs: 1114, f_score: 120.000000, nEnd: 1108
nRound: 13, nAvgMs: 1114, f_score: 120.000000, nEnd: 1123
nRound: 14, nAvgMs: 1114, f_score: 120.000000, nEnd: 1108
nRound: 15, nAvgMs: 1114, f_score: 120.000000, nEnd: 1123
SSE版本
nRound: 1, nAvgMs: 327, f_score: 120.000000, nEnd: 327
nRound: 2, nAvgMs: 319, f_score: 120.000000, nEnd: 312
nRound: 3, nAvgMs: 322, f_score: 120.000000, nEnd: 328
nRound: 4, nAvgMs: 319, f_score: 120.000000, nEnd: 312
nRound: 5, nAvgMs: 318, f_score: 120.000000, nEnd: 312
nRound: 6, nAvgMs: 317, f_score: 120.000000, nEnd: 312
nRound: 7, nAvgMs: 318, f_score: 120.000000, nEnd: 327
nRound: 8, nAvgMs: 317, f_score: 120.000000, nEnd: 312
nRound: 9, nAvgMs: 317, f_score: 120.000000, nEnd: 312
nRound: 10, nAvgMs: 316, f_score: 120.000000, nEnd: 312
nRound: 11, nAvgMs: 317, f_score: 120.000000, nEnd: 328
nRound: 12, nAvgMs: 317, f_score: 120.000000, nEnd: 312
nRound: 13, nAvgMs: 316, f_score: 120.000000, nEnd: 312
nRound: 14, nAvgMs: 317, f_score: 120.000000, nEnd: 328
nRound: 15, nAvgMs: 317, f_score: 120.000000, nEnd: 312
測試代碼中其他部分的性能消耗之前單獨測試大約耗時142ms,SSE版本性能提升大約爲(1114 - 142)/ (317 - 142) = 5.5倍。
SSE上入並不算很難,主要是數據的封裝,後續會分享在優化實際算法中的應用。

 

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