【算法隨記七】巧用SIMD指令實現急速的字節流按位反轉算法。

  字節按位反轉算法,在有些算法加密或者一些特殊的場合有着較爲重要的應用,其速度也是一個很是關鍵的應用,好比一個byte變量a = 3,其二進制表示爲00000011,進行按位反轉後的結果即爲11000000,即十進制的192。還有一種經常使用的應用是int型變量按位反轉,其基本的原理和字節反轉相似,本文僅以字節反轉爲例來比較這個算法的實現。算法

  一種最爲傳統和直接的算法實現以下:函數

unsigned char Reverse8U(unsigned char x) { x = (x & 0xaa) >> 1 | (x & 0x55) << 1; x = (x & 0xcc) >> 2 | (x & 0x33) << 2; x = (x & 0xf0) >> 4 | (x & 0x0f) << 4; return x; }

  咱們對大數據進行測試,測試的代碼以下:測試

void Byte_Reverse_01(unsigned char *Src, unsigned char *Dest, int Length) { for (int Y = 0; Y < Length; Y++) { Dest[Y] = Reverse8U(Src[Y]); } }

  當Length=100000000(一億)時,上面的代碼大概用時470毫秒,咱們稍微更改下函數的樣式,更改以下:大數據

unsigned char Reverse8U(unsigned int x) { x = (x & 0xaa) >> 1 | (x & 0x55) << 1; x = (x & 0xcc) >> 2 | (x & 0x33) << 2; x = (x & 0xf0) >> 4 | (x & 0x0f) << 4; return x; }

  仍是使用Byte_Reverse_01的代碼,神奇的結果顯示速度一會兒就跳到220ms,快了一倍多。其實這個看下反彙編的代碼就能夠看到問題所在了,主要是前面的代碼使用了寄存器的低位,在32位的環境下不是頗有效。優化

  注意C語言中默認是傳值,因此在函數體內改變了x變量的值,並不會產生其餘的什麼問題,直接返回這個x不會影響Src中的數據。加密

  第二步改動,咱們試着4路並行看看,即以下代碼:spa

void Byte_Reverse_02(unsigned char *Src, unsigned char *Dest, int Length) { int BlockSize = 4, Block = Length / BlockSize; for (int Y = 0; Y < Block * BlockSize; Y += BlockSize) { Dest[Y + 0] = Reverse8U(Src[Y + 0]); Dest[Y + 1] = Reverse8U(Src[Y + 1]); Dest[Y + 2] = Reverse8U(Src[Y + 2]); Dest[Y + 3] = Reverse8U(Src[Y + 3]); } for (int Y = Block * BlockSize; Y < Length; Y++) { Dest[Y] = Reverse8U(Src[Y]); } }

  四路並行,一個是可讓編譯器編譯出能更充分利用指令級並行的指令(即在同一個指令週期內完成2個或多個指令),二是在必定程度上減小了循環變量計數的耗時,雖然這個對大循環不明顯,可是在本例這種輕計算量的代碼裏仍是有必定做用的。.net

  算法的速度變爲大概195ms,提速不是很明顯。code

  下一步改進,咱們知道,現代編譯器對字節變量的處理其實速度可能還不如處理int類型,所以,咱們考慮把這個四個字節的反轉用一個int類型的變量也一次性實現,這能夠用下面的代碼實現:blog

unsigned int Reverse32I(unsigned int x) { x = (((x & 0xaaaaaaaa) >> 1) | ((x & 0x55555555) << 1)); x = (((x & 0xcccccccc) >> 2) | ((x & 0x33333333) << 2)); x = (((x & 0xf0f0f0f0) >> 4) | ((x & 0x0f0f0f0f) << 4)); return x; }

  注意這裏起名叫Reverse32I其實不是很適合,畢竟他不是反轉32位數,但你知道就能夠了。

  測試代碼以下:

void Byte_Reverse_03(unsigned char *Src, unsigned char *Dest, int Length) { int BlockSize = 4, Block = Length / BlockSize; for (int Y = 0; Y < Block * BlockSize; Y += BlockSize) { *((unsigned int *)(Dest + Y)) = Reverse32I(*((unsigned int *)(Src + Y))); } for (int Y = Block * BlockSize; Y < Length; Y++) { Dest[Y] = Reverse8U(Src[Y]); } }

  測試結果顯示執行耗時爲65ms,靠,速度提升了三四倍。

  接下來,咱們考慮另外的實現方法,由於byte只有256個不一樣的數,所以,咱們也能夠直接用查表的方式來實現,這個表能夠實時計算(耗時能夠忽視),也能夠靜態給出,前人已經給給出了,這裏我直接貼出來:

static const unsigned char BitReverseTable256[] = { 0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, 0x10, 0x90, 0x50, 0xD0, 0x30, 0xB0, 0x70, 0xF0, 0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8, 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8, 0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4, 0x64, 0xE4, 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4, 0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC, 0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC, 0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2, 0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2, 0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA, 0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA, 0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, 0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6, 0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE, 0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1, 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1, 0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9, 0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9, 0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5, 0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5, 0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 0xED, 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD, 0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3, 0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3, 0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB, 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB, 0x07, 0x87, 0x47, 0xC7, 0x27, 0xA7, 0x67, 0xE7, 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7, 0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF, 0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F, 0xFF };

  最直接的查找代碼以下:

void Byte_Reverse_04(unsigned char *Src, unsigned char *Dest, int Length) { for (int Y = 0; Y < Length; Y++) { Dest[Y] = BitReverseTable256[Src[Y]]; } }

  測試耗時:70ms,速度也是不錯的。

  若是分四路並行測試,代碼以下:

void Byte_Reverse_05(unsigned char *Src, unsigned char *Dest, int Length) { int BlockSize = 4, Block = Length / BlockSize; for (int Y = 0; Y < Block * BlockSize; Y += BlockSize) { Dest[Y + 0] = BitReverseTable256[Src[Y + 0]]; Dest[Y + 1] = BitReverseTable256[Src[Y + 1]]; Dest[Y + 2] = BitReverseTable256[Src[Y + 2]]; Dest[Y + 3] = BitReverseTable256[Src[Y + 3]]; } for (int Y = Block * BlockSize; Y < Length; Y++) { Dest[Y] = BitReverseTable256[Src[Y]]; } }

  測試耗時:40ms,速度又有了大幅度的提升了。同時和前面的Byte_Reverse_02相比,明天提速比例徹底不在一個檔次,這是所以這裏代碼很是簡單,就是一個查找表,他很容易實現指令級的並行。

  還有一種方式,其實也相似於四路並行,以下所示:

void Byte_Reverse_06(unsigned char *Src, unsigned char *Dest, int Length) { int BlockSize = 4, Block = Length / BlockSize; for (int Y = 0; Y < Block * BlockSize; Y += BlockSize) { unsigned int Value = *((unsigned int *)(Src + Y)); *((unsigned int *)(Dest + Y)) = (BitReverseTable256[Value & 0xff]) | (BitReverseTable256[(Value >> 8) & 0xff] << 8) | (BitReverseTable256[(Value >> 16) & 0xff] << 16) | (BitReverseTable256[(Value >> 24) & 0xff] << 24); } for (int Y = Block * BlockSize; Y < Length; Y++) { Dest[Y] = BitReverseTable256[Src[Y]]; } }

  原本想利用int比byte處理起來快的特性,可是這樣處理有計算量增大了,結果耗時50ms,比四路並行反而慢了一點。

   在 c語言實現bit反轉的最佳算法-從msb-lsb到lsb-msb一文的回覆一欄中,我無心看到ytfhwfnh的回覆以下:

   我以爲查表法不錯,可是表太大了,建議改成半字節爲單元的查表。這樣,只須要16個uchar的表就夠了。查表,再翻轉高低半字節,再翻轉一個int32的4個字節。就能搞定了。

  他這個話的後續的再翻轉一個int32的4個字節在本例中正好不要,他提供的示例代碼以下:

LOCAL u_long ucharBitsListR2Ulong(u_char* ucBits) { const static u_char BitReverseTable16[] = { 0x0, 0x8, 0x4, 0xC, 0x2, 0xA, 0x6, 0xE, 0x1, 0x9, 0x5, 0xD, 0x3, 0xB, 0x7, 0xF }; u_long ret = 0; int i = 0; for (; i < 4; i++) { /* 獲取當前字節,高4位 */ u_char ucTmp = (ucBits[i] >> 4) & 0x0F; /* 查表得反轉的半字節,並轉爲u_long */ u_long ulTmp = BitReverseTable16[ucTmp]; /* 存入ret對應位置的低4位 */ ret [表情]= ulTmp << (i * 8); /* 獲取當前字節,低4位 */ ucTmp = ucBits[i] & 0x0F; /* 查表得反轉的半字節,並轉爲u_long */ ulTmp = BitReverseTable16[ucTmp]; /* 存入ret對應位置的高4位 */ ret [表情]= ulTmp << (i * 8 + 4); } return ret; }

  這個[表情]是 CSDN的特點,實際上他應該是| 運算符。

  咱們把他這個函數直接展開嵌入到循環中,造成了以下的利用16位進行查表的算法:

void Byte_Reverse_08(unsigned char *Src, unsigned char *Dest, int Length) { int BlockSize = 4, Block = Length / BlockSize; for (int Y = 0; Y < Block * BlockSize; Y += BlockSize) { unsigned int Value = *((unsigned int *)(Src + Y)); *((unsigned int *)(Dest + Y)) = (BitReverseTable16[(Src[Y + 0] >> 4) & 0x0F]) | (BitReverseTable16[Src[Y + 0] & 0x0F] << 4) | (BitReverseTable16[(Src[Y + 1] >> 4) & 0x0F] << 8) | (BitReverseTable16[Src[Y + 1] & 0x0F] << 12) | (BitReverseTable16[(Src[Y + 2] >> 4) & 0x0F] << 16) | (BitReverseTable16[Src[Y + 2] & 0x0F] << 20) | (BitReverseTable16[(Src[Y + 3] >> 4) & 0x0F] << 24) | (BitReverseTable16[Src[Y + 3] & 0x0F] << 28); } for (int Y = Block * BlockSize; Y < Length; Y++) { Dest[Y] = Reverse8U(Src[Y]); } }

  很惋惜,我沒有獲得我想要的效果,這段代碼結果耗時110ms,比256個元素的查找錶慢。

  那一樣,咱們用四路並行實現他們試下,即以下代碼:

void Byte_Reverse_08(unsigned char *Src, unsigned char *Dest, int Length) { int BlockSize = 4, Block = Length / BlockSize; for (int Y = 0; Y < Block * BlockSize; Y += BlockSize) { Dest[Y + 0] = (BitReverseTable16[(Src[Y + 0] >> 4) & 0x0F]) | (BitReverseTable16[Src[Y + 0] & 0x0F] << 4); Dest[Y + 1] = (BitReverseTable16[(Src[Y + 1] >> 4) & 0x0F]) | (BitReverseTable16[Src[Y + 1] & 0x0F] << 4); Dest[Y + 2] = (BitReverseTable16[(Src[Y + 2] >> 4) & 0x0F]) | (BitReverseTable16[Src[Y + 2] & 0x0F] << 4); Dest[Y + 3] = (BitReverseTable16[(Src[Y + 3] >> 4) & 0x0F]) | (BitReverseTable16[Src[Y + 3] & 0x0F] << 4); } for (int Y = Block * BlockSize; Y < Length; Y++) { Dest[Y] = Reverse8U(Src[Y]); } }

  一樣道理,這樣又要快一點了,能作到75ms,但比256個查找表的多路並行仍是要慢的。

  這是能夠理解的,通常來講,查找表越少,一樣的查找次數耗時則越小,這主要得益於小的查找表有着較小的cache miss,可是咱們注意到,上述16個元素的查找表的查找次數多了一倍,並且也多了不少移位和或運算,所以,總的耗時並無減小。

  可是,到這裏,就出現了一個令我很是感興趣的話題了,我一直在思考如何利用SIMD指令實現快速的查表問題,後來獲得的結論是,這個基本上不可行,對應SSE,除非幾個特殊的表,一個狀況就是,這個查找表只有16個元素,並且表的類型是byte類型,這個時候,咱們就能夠利用_mm_shuffle_epi8指令進行快速的shuffle,此時的效果就比直接查表要快了不少了。

  那麼仔細的觀察上面的代碼,除了查表以外,其餘的計算太容易用SSE相應的指令實現了,或計算,並計算,注意移位計算SSE指令的_mm_srli_si128 、_mm_slli_si128並非按位移位的,他是按照字節進行的移位,這個時候咱們可借用_mm_srli_epi16或者_mm_srli_epi32來實現相同的功能。

  此時,可編制以下的SSE代碼實現相同的功能:

void Byte_Reverse_09(unsigned char *Src, unsigned char *Dest, int Length) { int BlockSize = 16, Block = Length / BlockSize; __m128i Table = _mm_loadu_si128((__m128i *)BitReverseTable16); __m128i Mask = _mm_set1_epi8(0x0F); for (int Y = 0; Y < Block * BlockSize; Y += BlockSize) { __m128i SrcV = _mm_loadu_si128((__m128i *)(Src + Y)); __m128i High = _mm_and_si128(_mm_srli_epi16(SrcV, 4), Mask);        // 高四位
        __m128i Low = _mm_and_si128(SrcV, Mask);                            // 低四位
        High = _mm_shuffle_epi8(Table, High);                                // 查找表
        Low = _mm_shuffle_epi8(Table, Low); _mm_storeu_si128((__m128i *)(Dest + Y), _mm_or_si128(High, _mm_slli_epi16(Low, 4)));    // 合併保存
 } for (int Y = Block * BlockSize; Y < Length; Y++) { Dest[Y] = Reverse8U(Src[Y]); } }

  此時函數的執行速度提升到了25ms左右,而且咱們看到,這裏實際上是實質上就沒有任何的查表工做了,也不存在所謂的cache miss的。

  在此基礎上,咱們能夠將這個函數擴展到使用AVX優化,AVX支持一次性處理32個字節的數據,比SSE還要擴展一倍,而且如今大部分CPU已經支持AVX2了,嘗試一下:

void Byte_Reverse_10(unsigned char *Src, unsigned char *Dest, int Length) { int BlockSize = 32, Block = Length / BlockSize; __m256i Table = _mm256_loadu_si256((__m256i *)BitReverseTable32); __m256i Mask = _mm256_set1_epi8(0x0F); for (int Y = 0; Y < Block * BlockSize; Y += BlockSize) { __m256i SrcV = _mm256_loadu_si256((__m256i *)(Src + Y)); __m256i High = _mm256_and_si256(_mm256_srli_epi16(SrcV, 4), Mask);        // 高四位
        __m256i Low = _mm256_and_si256(SrcV, Mask);                            // 低四位
        High = _mm256_shuffle_epi8(Table, High);                                // 查找表
        Low = _mm256_shuffle_epi8(Table, Low); _mm256_storeu_si256((__m256i *)(Dest + Y), _mm256_or_si256(High, _mm256_slli_epi16(Low, 4)));    // 合併保存
 } for (int Y = Block * BlockSize; Y < Length; Y++) { Dest[Y] = Reverse8U(Src[Y]); } }

  速度也基本在25ms左右波動,區別和SSE不是不少大。

  最後,咱們在返回到最開始的unsigned char Reverse8U(unsigned char x)函數,咱們發現,這個函數內部的算法自然的就支持SSE並行化處理,咱們能夠稍微修改下語法就能夠獲得對應的SSE版本函數,以下所示:

void Byte_Reverse_11(unsigned char *Src, unsigned char *Dest, int Length) { int BlockSize = 16, Block = Length / BlockSize; for (int Y = 0; Y < Block * BlockSize; Y += BlockSize) { __m128i V = _mm_loadu_si128((__m128i *)(Src + Y)); V = _mm_or_si128(_mm_srli_epi16(_mm_and_si128(V, _mm_set1_epi8(0xaa)), 1), _mm_slli_epi16(_mm_and_si128(V, _mm_set1_epi8(0x55)), 1)); V = _mm_or_si128(_mm_srli_epi16(_mm_and_si128(V, _mm_set1_epi8(0xcc)), 2), _mm_slli_epi16(_mm_and_si128(V, _mm_set1_epi8(0x33)), 2)); V = _mm_or_si128(_mm_srli_epi16(_mm_and_si128(V, _mm_set1_epi8(0xf0)), 4), _mm_slli_epi16(_mm_and_si128(V, _mm_set1_epi8(0x0f)), 4)); _mm_storeu_si128((__m128i *)(Dest + Y), V); } for (int Y = Block * BlockSize; Y < Length; Y++) { Dest[Y] = Reverse8U(Src[Y]); } }

  這個版本也是至關的快的,大約用時28ms左右,並且不佔用任何其餘內存。

  固然,以上的時間比較只基於本人的一臺電腦,在不一樣的CPU系列當中,各算法之間的耗時比例是不太相同的。有些甚至出現了相反的現象,總的來講,用多路並行256個元素的查找表方式是最爲穩妥和誇平臺的,若是在PC段,則能夠考慮時候用SIMD優化的版本。

  各版本的總和速度比較以下:

      

 

  附本文完整測試代碼供有興趣的朋友研究:https://files.cnblogs.com/files/Imageshop/Byte_Reverse.rar

  既然是針對字節的數據處理,天然而然咱們想到能夠直接把他應用在圖像上,好比對lena圖像,應用這個算法獲得以下效果:

 

   後面一幅圖你還能看出他是lena嗎,可是確實能夠對後面的圖再次利用本算法,恢復出完整的lena圖,這也能夠算是最簡答的圖像加密算法之一吧。

        寫博不易,歡迎土豪打賞讚助。

相關文章
相關標籤/搜索