代碼優化-之-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 )
代碼優化-之-Base64編碼函數的極限優化挑戰
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.