代碼高效性和健壯性的權衡


這個是比較早, 09年4月份的事情了。整理文檔翻出來,覺得還有點意思.

 

當時CLIENT-SERVER的通訊封包格式有兩種方案

a. 以7E為開頭和結尾, PAYLOAD中所有7E的字節, 都在其后擴展一個BYTE, 寫為7E, 7D, (稱為轉義). 封包中不帶CHECKSUM, CRC等校驗用的字段

b. 以7E為開頭和結尾, 帶一個CHECKSUM字段, PAYLOAD中不進行7E->7E 7D的轉義.

幾個同事就這個通信封包格式, 采用方案一或方案二, 開會激烈討論了個把小時。
我原先反對轉義方案的出發點比較模糊, 只是覺得原先轉義的方案"不優雅"; 后來才想清楚了不優雅的"本質"在哪里.

 

所有的代碼, 可以在抽象意義上分作兩大塊, 兩者的着重點是不同的.

(1) 正常運行的代碼. 首要追求高效性,
    這個"高效性"如果從邏輯的角度來解釋, 那么一方面是"高效"地對正確的數據執行正確的算法(方法/策略), 另一方面是"高效"地找出異常, 然后丟給異常處理代碼去處理.

(2) 處理異常的代碼. 首要追求健壯性. 
    就是程序必須能從異常中自我恢復. 由於代碼多數時間跑的是"正常"邏輯, 少數情況下才不得不處理"異常", 所以"異常"處理的代碼中, 首要任務是健壯, 跑不死, 而高效性則是次要的.

 

那么回到轉義的策略上來看,原先的7E -> 7E 7D, 使得裝包和拆包的時候, 時間上都必須挨字節掃描過去, 空間上必須另開一塊內存, 這些"不優雅"的工作是為了應對網絡傳輸時包數據丟失. 包數據丟失是一個異常情況,而轉義策略本質上就是不論好包壞包,一棍子打死, 統統要經過轉義算法. 用上面的觀點解釋, 即"為了異常情況下的健壯性,犧牲了正常情況下的高效性".

而用Header + Length + CheckSum + Payload + Tailer的做法, 邏輯上是這樣的

if  ( 以Length為基礎, 得知CheckSum正確 和 Tailer正確)
{
    正確的包,走正常處理流程, 直接把Payload傳給上層邏輯處理

else
{
    錯誤的包,走異常處理流程,挨字節掃描下一個Header, 然后再算length, checksum, tailer等
}

這是在上面"正常->高效性 & 異常->健壯性"指導思想下的做法. 那么現在就剩最后一個問題, 計算checksum和轉義的工作相比, 哪一個更快? 如果轉義處理的效率, 比checksum更高,那么上面的假設就不成立了.

所以我做了個實驗, 代碼如下

代碼
BYTE *  pBuf1  =   new  BYTE[ 1024 ];
BYTE
*  pBuf2  =   new  BYTE[ 1024 * 2 ];
UINT8 sum 
=   0 ;
DWORD tStart 
=   0 , tEnd  =   0 ;

//  CODE 1, 轉義處理
tStart  =  GetTickCount();   //  WM上的毫秒級時間

for ( int  j  =   0 ; j  <   100000 ; j ++ )
{
    
for ( int  i1  =   0 , i2  =   0 ; i1  <   1024 ; i1 ++ , i2 ++ )
    {
        
if  (pBuf1[i1]  ==   0x7E   &&  pBuf1[i1 + 1 ==   0x7D )
        {
            pBuf2[i2] 
=  pBuf1[i1];
            i1
++ ;            
        } 
        
else
        {
            pBuf2[i2] 
=  pBuf1[i1];
        }
    }

tEnd 
=  GetTickCount();
        
printf(
" copy 1024 bytes * 100K times, use %d ms\n " , tEnd  -  tStart);

//  CODE 2, CHECK SUM
tStart  =  GetTickCount();

for ( int  j  =   0 ; j <   100000 ; j ++ )
{
    
for ( int  i  =   0 ; i  <   1024 ; i ++ )
    {
        sum 
+=  pBuf1[i];
    }
}

tEnd 
=  GetTickCount();

printf(
" check sum 1024 bytes *100K times,  use %d ms\n " , tEnd  -  tStart);

上面這段代碼,在SAMSUNG 2442 400MHz的CPU, WM 6.1系統上運行結果是

copy 1024 bytes * 100K times, use 11677 ms
check sum 1024 bytes *100K times,  use 7504 ms

所以, 一個正確的數據包, 經過CHECKSUM計算的時間, 比其經過轉義計算的時間要快得多, 僅為其64%.  這是手機上的情況, 服務器上的百分比不太清楚是什么樣,但至少有一點是肯定的,就是用CHECKSUM的方案比用轉義的方案,在正常邏輯情況下速度更快、內存開銷更少。當服務器同時處理十萬數量級網絡數據包的時候, 性能提升還是比較可觀的。

 

這篇文章的重點不在於哪個方案更嚴謹,或者上面的邏輯對不對,而是在於這么一個思想:

(1) 正常運行的代碼. 首要追求高效性,

(2) 處理異常的代碼. 首要追求健壯性. 

 


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com