软件开发最关心的三个指标:性能、内存、程序稳定性三方面。本文总结一下最近项目扫尾工作中的一些遭遇:
使用正确的哈希函数
道路的路况绘制,道路的颜色由三个ID唯一确定,他们存储在一个哈希表中。
上图是两种哈希函数的性能对比。badHashFunction的结果为蓝色,goodHashFunction的结果为红色曲线。
使用坏的哈希函数,执行DJB_hash的结果冲突可能性十分大,因此哈希的平均查找次数非常大,在性能很好的机器上拖动时也有明显的卡顿现象。
优化哈希函数,将三个ID的所有位数拼接成一个数字串,然后传入DJB_hash结果十分好,性能得到质的提升。
static unsigned int DJB_hash(int* buffer, int len) { unsigned int hash = 5381; int i = 0; while (i < len) { hash += (hash << 5) + buffer[i++]; } return (hash & 0x7FFFFFFF); } static unsigned int badHashFunction(const void* key) { rtic_t* ptr = (rtic_t*)key; int buffer[3]; buffer[0] = ptr->mapId - RTIC_MIN_MAPID; buffer[1] = ptr->kind; buffer[2] = ptr->middle; return DJB_hash(buffer, 3); } static unsigned int goodHashFunction(const void* key) { rtic_t* ptr = (rtic_t*)key; const int SIZE = 20; int buffer[SIZE] = {0}; int len = 0; int v = ptr->mapId; while (v && len < SIZE) { buffer[len++] = v % 10; v /= 10; } v = ptr->kind; while (v && len < SIZE) { buffer[len++] = v%10; v /= 10; } v = ptr->middle; while (v && len < SIZE) { buffer[len++] = v%10; v /= 10; } return DJB_hash(buffer, len); }
谨慎使用vector
Vector的好处就是动态增长,使用起来非常方便,只管不断地push_back,不用关心内存增加的细节。Vector稍微有经验的都知道,push_back之前应该先reserve。这么做效率更高!
最近查我们项目的样式配置模块,突然发现内存占用十分厉害,前后情况如下:
样式加载前内存情况:
样式加载后内存情况:
单纯样式模块占用600K。
一路追查下去,结果哭笑不得。我们自己实现了一套C风格的Vector,里面存储void*指针从而实现泛型。Vector内部reserve函数实现非常之坑爹,代码片段如下,每次reserve结果vector中至少有256个指针。
void TXVector::reserve(TXUINT32 capacity) { if (capacity <= _capacity) { return; } _capacity = capacity * 2; if (_capacity < 256) { _capacity = 256; }
上图为样式配置模块的数据结构,是个二维数组。当时因为赶项目进度,regionStyleList的每个成员内部有一个vector,然而vector只保存1-3个指针成员。RegionStyleList的数量很大,所以导致内存浪费十分严重。
优化时直接KISS(keep it simple and stupid)化,采用最简单的二维数组优化后,结果非常明显,内存降到82K。
注意空指针访问问题
背景:地图切换数据后,城市的路况映射表达到500k左右,于是我们对数组进行了延迟创建处理。就是因为这个优化引入了一个十分隐晦的BUG,灰度上线后IOS、Android两个平台都收到不同数量的crash日志。下面是android的日志:
********** Crash dump: ********** Build fingerprint: ‘samsung/t03gzs/t03g:4.1.2/JZO54K/N7100ZSDMA6:user/release-keys‘ pid: 10190, tid: 10190 >>> com.XX.map <<< signal 11 (SIGSEGV), fault addr 00000000 Stack frame #00 pc 0000e270 /system/lib/libc.so (memcpy) |
日志中黄色部分标识空指针访问越界,Crash代码行指向memcpy这一行:
这个空指针crash正常情况下不会发生,如下图我们升级因为要兼容旧数据,所以程序中有两种路径:
每条竖直路径表示我们预期的正常途径:全量更新时创建数组,增量更新时刷新数组。红色箭头路径表示非法路径:当A格式全量更新创建A格式数组,然后下次增量更新时跳至了B的路径去刷新B的数组,此时B的数组为空,从而空指针CRASH。
界面层的某种操作会触发红色路径!所以空指针crash带有随机性色彩。通过不断讨论我们最终归纳出了BUG必现的触发途径。当然了空指针BUG解决起来非常容易。
注意对程序边界条件处理
引擎重构以后,两个平台多了一种crash日志,android内容:
Build fingerprint: ‘samsung/m0zs/m0:4.1.2/JZO54K/I9300ZSEMC1:user/release-keys‘ pid: 26598, tid: 26598 >>> com.XXX.map <<< signal 11 (SIGSEGV), fault addr 04000000 Stack frame #00 pc 0000e264 /system/lib/libc.so (memcpy) Stack frame #01 pc 0001a780 /data/data/com.XXX.map/lib/libengine.so: Routine getScanEdges in jni/src/gc/SEA/SubPolygon.cpp:69 |
对应的代码行:
POD类型对象拷贝调用的是memcpy。看到这个结果我们怀疑是某种极端的面数据导致了引擎的crash,于是乎大家雄心勃勃,一起讨论了一个方案:写一个benchmark程序在内存中处理全国所有城市数据。但是跑了好几天也没办法复现,时间一点点过去,大家的意志力逐渐被消磨殆尽,crash日志还是越涨越多。
最终一个经验丰富的高工最终了问题的可能原因:SubPolygon没有特殊处理顶点为0的情况。原因是生成瓦片中,多边形使用软件方法裁剪时可能会生成顶点为0的多边形,然后进行绘制。
举个例子:int* ptr = new int[0]; ptr返回指针是不确定的,可能为空也可能不为空。malloc(0)返回的指针除了可以传入free函数之外不建议有其他操作,直接访问内存会出现随机性的结果。Linux上malloc(0)行为:http://www.cnblogs.com/xiaowenhu/p/3222709.html
教训:代码中增加一些合法性判断代码,覆盖各种边界处逻辑。它们绝对不会是Dead Code,因为它们什么时候起作用,你很难想象到或者根本没办法预料到。
代码中的魔鬼细节