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

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

你可能感兴趣的:(代码高效性和健壮性的权衡)