代码中的魔鬼细节

软件开发最关心的三个指标:性能、内存、程序稳定性三方面。本文总结一下最近项目扫尾工作中的一些遭遇:

使用正确的哈希函数

道路的路况绘制,道路的颜色由三个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,因为它们什么时候起作用,你很难想象到或者根本没办法预料到。

代码中的魔鬼细节

时间: 2024-10-09 14:38:25

代码中的魔鬼细节的相关文章

程序中的魔鬼数字

在代码中使用魔鬼数字(没有详细含义的数字.字符串等)将会导致代码难以理解,应该将数字定义为名称有意义的常量. 将数字定义为常量的终于目的是为了使代码更easy理解,所以并非仅仅要将数字定义为常量就不是魔鬼数字了.假设常量的名称没有意义,无法帮助理解代码,相同是一种魔鬼数字. 在个别情况下,将数字定义为常量反而会导致代码更难以理解,此时就不应该强求将数字定义为常量. 案例 // 魔鬼数字,无法理解3详细代表产品的什么状态 if (product.getProduct().getProductSta

GA代码中的细节

GA-BLX交叉-Gaussion变异 中的代码细节: 我写了一个GA的代码,在2005测试函数上一直不能得到与实验室其他同学类似的数量级的结果.现在参考其他同学的代码,发现至少有如下问题: 1.在交叉和变异的操作后,应对新产生的个体的每一维度有上下界限制: 2.交叉操作,随机在两个个体上进行交叉.若两个个体相同,则没有做实值交叉.之前的代码忽略了这个问题.新修改的代码中,若两个个体相同,则选择下一个个体进行交叉: 3.变异操作后,结果不一定更好.先留下来精英个体nRemain,再轮盘赌选择N-

强壮你的C和C++代码30个小细节

1 初始化局部变量 使用未初始化的局部变量是引起程序崩溃的一个比较普遍的原因, 2 初始化WINAPI 结构体 许多Windows API都接受或则返回一些结构体参数,结构体如果没有正确的初始化,也很有可能引起程序崩溃.大部分Windows API结构体都必须有一个cbSIze参数,这个参数必须设置为这个结构体的大小. 注意:千万不要用ZeroMemory和memset去初始化那些包括结构体对象的结构体,这样很容易破坏其内部结构体,从而导致程序崩溃. 3 检测函数输入参数有效性 在函数设计的时候

注意编码工作中的小细节

人们常说"细节决定成败". 编码工作中,同样需要关注细节. 本文将给出3个小实例来说明编码中关注细节的重要性,同时给出作者对如何注意编码细节的一点见解(说的不对,请指正). 例1 这个问题如此地显而易见,竟然没有被发现. List<int> numList = new List<int>(); numList.Add(3); numList.Add(1); numList.Add(4); numList.Add(2); numList.Add(5); numLi

关于项目中遇到的细节化的原则

"时间都去哪儿了",每当我听到这首歌的时候,都会感慨,时间飞梭,恍如流逝,真的过的很快!一眨眼,来北京快1年了,目前从事着我喜欢的热爱的编程工作,虽然比不上JAVA,C等强类型语言,但PHP改变了我的生活,改变了我原先的运行轨迹!朝着目标一步一步前进! 新手,菜鸟目前来形容我再合适不过了,我承认我的确是一名名副其实的小白,作为一名菜鸟级的程序员,路还很遥远,但是学习到的东西不可谓不多,尤其是团队合作中遇到的问题因为一个人的错误会浪费很多时间和精力! 就在上个星期,我所在的单位因客户比较

java代码中init method和destroy method的三种使用方式

在java的实际开发过程中,我们可能常常需要使用到init method和destroy method,比如初始化一个对象(bean)后立即初始化(加载)一些数据,在销毁一个对象之前进行垃圾回收等等. 周末对这两个方法进行了一点学习和整理,倒也不是专门为了这两个方法,而是在巩固spring相关知识的时候提到了,然后感觉自己并不是很熟悉这个,便好好的了解一下. 根据特意的去了解后,发现实际上可以有三种方式来实现init method和destroy method. 要用这两个方法,自然先要知道这两

从Android代码中来记忆23种设计模式

我的简书同步发布:从Android代码中来记忆23种设计模式 相信大家都曾经下定决心把23种设计模式牢记于心,每次看完之后过一段时间又忘记了~,又得回去看,脑子里唯一依稀记得的是少数设计模式的大致的定义.其实,网上很多文章讲得都非常好,我也曾经去看过各种文章.也曾一直苦恼这些难以永久记下的设计模式,直到我接触到了<Android源码设计模式解析与实战>--何红辉与关爱明著,发现原来其实我们在Android中都接触过这些设计模式,只是我们不知道而已.既然我们都接触过,我们只需一一对号入座,对设计

提取代码中的部分代码字段

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-

使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用

使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用 选择调用的进程为 24 i386 getuid sys_getuid1647 i386 getgid sys_getgid16 使用库函数API方式 使用C代码中嵌入汇编代码方式