www.Walzer.cn - Tech & Management Blog

Focus on mobile dev
本博客文章,未在标题中写明转载的, 均为原创.
所谓高手,也就是熟悉别人制定的游戏规则、并且能在规则内跳舞的人。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

代码高效性和健壮性的权衡

Posted on 2009-12-14 17:29  Walzer  阅读(1431)  评论(0编辑  收藏  举报

这个是比较早, 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) 处理异常的代码. 首要追求健壮性.