代碼優化-之-Base64編碼函數的極限優化挑戰

           代碼優化-之-Base64編碼函數的極限優化挑戰
                  [email protected]   2007.07.27
tag:速度優化,Base64,CPU緩存優化,代碼優化,查找表,彙編,SSE、SSE2優化,並行
 
摘要: Base64編碼是很常用的一種把二進制數據轉換爲字符串的算法;
本文章對Base64的編碼函數進行了各種優化嘗試,目標是極限編碼速度!
並對優化過程中使用的方法進行了詳細說明(主要使用了查表優化);
(2008.01.07 在文章中添加了Base64解碼函數base64_decode,其實文章末尾提供的下載源代碼中已經有該函數了,這次只是把其代碼貼在文章中;)
(2007.11.25 重新實現base64_encode3_sse2_prefetch的內存預讀優化;由於加了一條內存組成雙通道,並行測試成績都提高了5-10%)
(2007.09.07 添加base64_encode3_sse2_prefetch的實現,測試內存預讀的效果,但效果不太好。)
(2007.07.27 覺得只使用64字節表的彙編優化也是有其意義的,所以添加了base64_encode1_asm實現)
        
正文: 
  代碼使用C++;涉及到彙編優化的時候假定爲x86平臺;
  測試平臺:(CPU:AMD64x2 4200+(2.37G);內存:DDR2 677(雙通道);C/C++編譯器:VC2005)
  操作系統:WindowsXP    
  (  警告:代碼移植到其他體系的CPU時需要重新考慮字節順序的大端小端問題!  實際項目中如果想要用這裏的代碼,建議選用64字節表的那個優化版本; )
 
A:本文章的來源和起因:
   cpper編程論壇( www.cpper.com/c )以前進行過一次Base64編碼器速度競賽
( http://www.cpper.com/c/t665.html 和 http://www.cpper.com/c/t694.html );
當時我沒有參加,後來應管理者要求看看能不能在結果上繼續優化,那時就簡單
了點代碼嘗試,但沒能得到更好的結果;
   最近因爲工作中需要用到Base64,就拿了其競賽結果(Base64EncodeSXMNew版本)
來改寫;由於拿人手短,而且以前有答應看看是否還有改進的餘地;所以空餘時間
就在這個基礎之上進行了一些速度優化改進嘗試,本文章就是本次嘗試的結果;
   (聲明:本文章引用這次競賽和其代碼得到了cpper管理者的授權)
   嘗試了一些代碼後,我在CSDN程序員網站( www.csdn.net )開貼徵集最快的Base64
編碼函數:
  http://community.csdn.net/Expert/topic/5665/5665480.xml?temp=.4219171 ,
以求獲得更多的思路和使已有的優化策略獲得更多的驗證機會;發帖的朋友對本文章
的形成也起到了重要的作用,某些版本的函數速度得到了提高,某些在我的AMDx2
CPU上運行很快的版本、在網友的奔騰4 CPU上運行減慢(放棄了一簇版本)、由討論
而產生的新的函數版本等等;本文章也是對這次討論的一些簡要總結(某些網友的實現
版本或策略沒有整合到本文章中,請到csdn論壇直接查看);    
 
B:Base64編碼原理簡要說明
   有時爲了更好的傳遞或保存二進制數據,需要先把二進制數據轉換成純文本的編碼方式。
最容易想到的方案就是直接轉換成16進制的文本方式;即把一個字節(8bit)先分成兩
個4bit(值域[0..15]),然後映射到'0'--'9''A'--'F'這16個字符編碼中; 那麼一個字節
的數據轉換爲了兩個字符,也就是說數據轉換後變爲原數據大小的兩倍!
   Base64的也能完成這個任務,但更節省空間,轉換後的文本數據只是原數據大小的4/3倍;
這是怎麼做到的呢?   將原數據3個字節一組(3x8bit),按一定方式分組成4個6bit(值
域[0..63]);  然後將[0..25]的值映射到'A'--'Z',將[26..51]的值映射到'a'--'z',
將[52..61]的值映射到'0'--'9',將62的值映射爲'+',將63的值映射爲'/'; 由於原數據不
一定正好是3的倍數,所以分組後的4個值中有可能沒有被分配bit位的情況,沒有分配bit位
的值輸出數據填充'='。
bit位分組方式示意:
  // 3x8bit
  // |------------------|------------------|------------------|
  // |      a[0..7]     |     b[0..7]      |     c[0..7]      |
  // |------------------|------------------|------------------|
  //
  // to 4x6bit
  // |------------------|------------------|------------------|------------------|
  // |      a[2..7]     |b[4..7]+a[0..1]<<4|c[6..7]+b[0..3]<<2|     c[0..5]      |
  // |------------------|------------------|------------------|------------------|

C:一個基本實現和速度測試框架
使用了較大的數據量多次測試取平均值;
//編碼函數每秒編碼出的數據量:
// base64_encode0                     109.0 MB/s




測試代碼:


D.優化to_base64char函數
  to_base64char有很多的條件分支,在當前的CPU上會嚴重的降低性能;
  分析該函數有這樣的特徵: 函數的輸入數據允許的取值個數很小(只能取64個值中的一個);
單個輸入值對應的返回值固定;
  那麼我們可以建立一個數組的表格,該數組有64個元素,每個元素的值等於該元素的序
號(假定數組序號從0開始)作爲to_base64char參數時的返回值;
  即:  unsigned char BASE64_CODE[64];
       其中BASE64_CODE[i]=to_base64char(i); // i屬於[0..63]
  那麼對於這樣的代碼: output[0]=to_base64char(input[0]>>2);
          可以化簡爲: output[0]=BASE64_CODE[input[0]>>2];
  這就是利用查表來替代計算的優化方法!
  (提示:在不同的需求下,表需要靈活的構造)
 
base64_encode0使用查詢表改進後的新代碼:
//編碼函數每秒編碼出的數據量:
// base64_encode0_table               294.6 MB/s


E:在當前的32比特CPU上一次寫入4字節將獲得更好的性能;而輸入數據的地方,可以先轉化
成32比特整形數據再做各種複雜的位運算有利於編譯器的優化(各個PC平臺的差異可能比較大);
//編碼函數每秒編碼出的數據量:
// base64_encode1                     533.3 MB/s



F:減少位操作複雜度的一個方案:預先交換輸入數據的字節順序
//編碼函數每秒編碼出的數據量:
// base64_encode1_bswap               579.2 MB/s


G:使用64字節表的彙編優化版本base64_encode1_asm
//編碼函數每秒編碼出的數據量:
// base64_encode1_asm                 674.6 MB/s


H:爲了減少計算量,我們可以嘗試適量增大表的大小(空間換時間)
  對於這樣的代碼: output0=BASE64_CODE[input0/4];
  我想改寫成這樣: output0=BASE64_CODE_SHIFT2[input0];
  那麼BASE64_CODE_SHIFT2應該怎樣定義呢?由於output0=BASE64_CODE_SHIFT2[input0]=BASE64_CODE[input0/4];
  有BASE64_CODE_SHIFT2[i]=BASE64_CODE[i/4]; //i屬於[0.255]
  對於這樣的代碼: output3=BASE64_CODE[input2 & 0x3F];
  我想改寫成這樣: output3=BASE64_CODE_EX[input2];
  那麼BASE64_CODE_EX[i]=BASE64_CODE[i & 0x3F]; //i屬於[0.255]
  爲了能夠簡化output1和output2的計算,擴大BASE64_CODE_EX到4k,參見源代碼:
//編碼函數每秒編碼出的數據量:
// base64_encode2                     701.6 MB/s



I:使用更大的表來換取更快的速度!
  前面的代碼中6bit的數據查表可以得到8bit的輸出數據,那麼我可以構造一個更大的表,
一次使用12bit的數據查表得到16bit的輸出數據!
   原代碼: unsigned int output0=BASE64_CODE[input0 >> 2];
           unsigned int output1=BASE64_CODE[((input0 << 4) | (input1 >> 4)) & 0x3F];
   想要的新代碼: output_0_1=BASE64_WCODE[(input0<<4) | (input1>>4)]
     有 BASE64_WCODE[(input0<<4)|(input1>>4)]= BASE64_CODE[input0 >> 2]
             | (BASE64_CODE[((input0 << 4) | (input1 >> 4)) & 0x3F] << 8);   
     令i==(input0<<4) | (input1>>4); 則有: input0==i>>4; (input>>4)==i&0F;
     即:BASE64_WCODE[i]=BASE64_CODE[(i>>4)>>2]
             | (BASE64_CODE[ (((i>>4)<< 4) | i&0F) & 0x3F ] << 8); 
        BASE64_WCODE[i]=BASE64_CODE[i>>6] | (BASE64_CODE[i & 0x3F]<<8);//i屬於[0..4095]
   (當你熟悉用建數據表來表達計算的時候,這些推導反而顯得累贅;
    對於這裏的表的建立,只需要注意表的序號就是需要替換的函數(也可以是假想的函數)的參數,
    數據表中的值就是該輸入參數時函數的返回值,這樣就可以直接寫出表的建立表達式。)
   爲了不增加新的表,我們在的BASE64_WCODE表的基礎上來得到計算output_2_3的表達式;
        output_2_3=BASE64_WCODE[((input1<<8) | input2) & 0x0FFF];
//編碼函數每秒編碼出的數據量:
// base64_encode3                     791.3 MB/s


(聲明:在實際項目中Base64編碼函數很少會成爲瓶頸;項目中應該使用性能剖分工具來
定位程序熱點,集中精力優化這些瓶頸;不過本文章的目的正好是要看看對它的極致優
化所能使用的方案:)
如果使用更大的表(256K),一次使用16bit做查詢,可以更好的化簡計算
(但由於表太大可能不能裝入CPU的一級緩存,所以函數運行可能會很慢)
//編碼函數每秒編碼出的數據量:
// base64_encode_256K                 409.8 MB/s


嘗試一下24bit的查詢表,(1<<24)*4字節(64M),恐怖的表大小!
(警告:base64_encode_256K和base64_encode_64M函數只是作爲例子給出,並不建議真的使用)
//編碼函數每秒編碼出的數據量:
// base64_encode_64M                  111.3 MB/s



J:現在在base64_encode3的基礎上使用匯編來進行優化;
想法是儘量壓縮寄存器的使用,然後多個寄存器就能同時執行多路,增加了指令的併發能力;
(
說明: 剛開始寫過雙8K表的C++實現和其彙編優化實現,在AMDx2上速度很快,但覺得16k的表太
浪費,而且在奔騰4等CPU上速度降低嚴重,經歷過幾個版本(也有比較平衡的);
但它們比起base64_encode3_asm來就遜色了,所以就略去了。
  這裏的版本(8K表)base64_encode3_asm在各種CPU上的適應性應該不錯(但我還沒有機會測試過)
)
//編碼函數每秒編碼出的數據量:
// base64_encode3_asm                1061.3 MB/s



K:使用sse和sse2的寫緩存優化
警告: 優化寫緩衝需要滿足的條件是寫入的數據量比較大或者需要寫入的數據不需要很快就訪問,
從而避免了寫入的數據被CPU自動緩衝而"污染"緩存;(如果條件不滿足,“優化”反而會變成劣化)
提示: 在不支持SSE和 SSE2指令的CPU上函數將不能執行
  使用sse中的movntq指令來改寫_base64_encode3_line_asm爲_base64_encode3_line_sse;
代碼如下:
//編碼函數每秒編碼出的數據量:
// base64_encode3_sse                1205.6 MB/s
//(在奔騰4上使用該優化版本可能反而比base64_encode3_asm版本慢,需要測試一下)



  使用sse2中的movnti指令來改寫_base64_encode3_line_asm爲_base64_encode3_line_sse2;
代碼如下:
//編碼函數每秒編碼出的數據量:
// base64_encode3_sse2               1340.6 MB/s


L:使用軟件預讀指令prefetchnta來進一步優化base64_encode3_sse2:
 
//編碼函數每秒編碼出的數據量:
// base64_encode3_sse2_prefetch      1404.73 MB/s



M:Base64編碼函數的並行化
  Base64編碼函數其實很容易實現並行算法,把數據切割成幾段讓多個CPU執行就可以了;
  (既然是追求最快的速度,而且現在多核的普及已成必然,所以不增加Base64編碼函數的
並行版本測試有點說不過去:)   這裏利用CWorkThreadPool類來並行執行任務;  參見我
的Blog文章《並行計算簡介和多核CPU編程Demo》,裏面有CWorkThreadPool類的完整源代碼)
  將Base64編碼函數並行執行的代碼:

假設需要測試base64_encode3_sse2在並行情況下的速度:
  以前的調用代碼: base64_encode3_sse2(pdata,data_size,out_pcode);
  並行改成這樣調用就可以了:
  parallel_base64_encode(base64_encode3_sse2,pdata,data_size,out_pcode);
( 附:Base64解碼函數

)
N:測試的結果放到一起:
//todo:代碼在不同CPU上的速度差異的簡單分析 等待更多平臺的測試結果
////////////////////////////////////////////////////////////////////////////////
//測試平臺I7:(CPU:Intel i7 920(2.66G);    內存:DDR3 1333(三通道); 編譯器:VC2005)
//測試平臺A2:(CPU:AMD64x2 4200+(2.37G);   內存:DDR2 677(雙通道);  編譯器:VC2005)
//測試平臺C2:(CPU:Intel Core2 4400(2.00G);內存:DDR2 667(雙通道);  編譯器:VC2005)
//測試平臺AS:(CPU:AMD Sempron2800+(1.61G);內存:DDR 400;           編譯器:VC2005)
//測試平臺IX:(CPU:Intel Xeon(x4)(2.33G);  內存: ? ;               編譯器:VC2005)
//測試平臺Q6:(CPU:Intel Q6600(x4)(2.4G);  內存:DDR2 800(單通道);  編譯器:VC2005)
//測試平臺IC:(CPU:Intel Celeron(2.1G);    內存:DDR 333 ;          編譯器:VC2005)
////////////////////////////////////////////////////////////////////////////////
//每秒編碼出的數據量測試(MB/s)
//========================================================================
//                        A2   C2   IX   Q6   AS   IC   I7
//========================================================================
// base64_encode0        109  103  120  124   74   69  125
// base64_encode0_table  295  679  818  829  204  386 1014
// base64_encode1        533  618  733  755  391  409  884
// base64_encode1_bswap  579  496  586  604  403  397  727
// base64_encode_256K    410  567  661  685  237   26  964
// base64_encode_64M     111  135  162  164   74   21  279
//
// base64_encode1_asm    675  532  641  660            793
// base64_encode2        702  771  911  938  489  413 1033
// base64_encode3        791 1098 1359 1379  553  424 1667
// base64_encode3_asm   1061 1116 1343 1391  703  417 2162
// base64_encode3_sse   1206 1110 1297 1339  841  660 1888
// base64_encode3_sse2  1341 1160 1367 1409  943  685 2021
// base64_encode3_sse2_prefetch
//                      1405 1146      1382           1876
//
// 多路並行執行測試:
// base64_encode1_asm   1321 1058 1693 1486           3186
// base64_encode2       1366 1284 1668 1481           3823
// base64_encode3       1483 1315 1681 1478           6373
// base64_encode3_asm   1796 1317 1668 1480           7318
// base64_encode3_sse   2228 1978 2812 2408           8448
// base64_encode3_sse2  2520 2019 2845 2425           8685
// base64_encode3_sse2_prefetch
//                      2643 2040      2316           6682
//////////////////////////////////////////////////////////////////////////


//A2/C2/Q6和I7由我提供,IX由網友cat提供,IC和AS由網友constantine提供

(歡迎提交這些代碼在你的電腦上的測試成績(說明測試用的CPU和內存信息);歡迎提出改進意見)
( 本文章涉及到的測試源代碼下載: http://cid-10fa89dec380323f.office.live.com/self.aspx/.Public/base64%5E_test.zip )


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