技术开发 频道

内存泄漏检测程序的算法优化

  4、 优化方法

  在对程序进行优化前,首先要了解该程序的性能瓶颈所在之处。通过分析,发现获取堆栈跟踪信息的第3步——调用system函数执行addr2line命令的时间开销最大。但是,这一步是把函数内存地址转换成函数具体信息的必由之路,目前在Linux环境下还没有其他替代方案。既然无法避免,那么如何提高这一步的效率也就成了优化程序性能的关键。

  优化方式一:并行处理

  通过分析,我们发现,在第1步中调用backtrace函数和backtrace_symbols函数可以一次获取多层堆栈地址信息,而每一层堆栈地址信息的转换处理可以互不干扰。因此,可以考虑用多线程方式来同时处理每一层堆栈信息,需要注意的一点是,每个线程都需要使用单独的文件来保存转换后的信息。于是修改代码如下:

pthread_t pid[MAX_LEVEL]; //定义多线程对象
for( i = 0; i < trace_size && i< MAX_LEVEL; i++ )
{
 p_leak1
->callstack [i] = (void *)malloc(strlen(messages[i])+1);
 sprintf(p_leak1
->callstack [i],"%s",messages[i]);
//并发执行getfunc子函数来获取具体函数信息
 
if(pthread_create(&pid[i],NULL,getfunc,(void *)&(p_leak1->callstack[i]))!=0)
 
break;
 }
p_leak1
->level = i;
for( i = 0; ilevel; i++ )
pthread_join(pid
[i],NULL); //等待每一层的转换处理线程结束

  第3步中调用system函数执行addr2line命令来保存具体函数信息到文件中,每个线程使用的文件都不能冲突,因此可以最多用10个文件来同时进行转换操作。

  通过并行处理,理论上最高可以提高程序10倍的性能。修改后进行调用测试,发现程序启动速度得到有效的提高,但是仍然需要20~30分钟的时间,其性能依然达不到我们的需求。

  优化方式二:延后处理

  通过分析,进一步发现,达梦服务器申请内存时插入的很多信息结点,在释放的时候又会删除掉,这些结点都进行了转换操作。而之所以要进行堆栈地址信息的转换,仅仅只是为了保存未释放信息结点的详细堆栈信息到文件中,对于那些保存之前已经删除的信息结点,进行的转换过程都是多余的操作。因此,可以在申请内存时调用的add_leak_point接口中把转换过程的2~4步省略掉,只在达梦服务器调用write_leak_to_file函数接口保存当前未释放信息结点的堆栈信息到文件中时,才对信息结点中的中间结果进行转换,每个信息结点转后之后都将nChanged标识置为1,避免下一次保存时重复操作,这样就可以尽量过滤掉那些中途删掉的信息结点的转换开销。

  修改程序,把转换过程的2~4步从内存泄漏检测动态链接库的add_leak_point函数接口转移到write_leak_to_file函数接口中,修改后代码如下:

if(!p_leak1->nChanged)
{
  pthread_t pid
[MAX_LEVEL];
  
for(i=0;ilevel;i++)
    {
      
if(pthread_create(&pid[i],NULL,getfunc,(void *)&(p_leak1->callstack[i]))!=0)
        
break;
      
else
        p_leak1
->nChanged = 1; //设置已转换标志
    }
    
for( i = 0; ilevel; i++ )
      pthread_join(pid
[i],NULL);
}

  通过这次修改,在达梦服务器没有调用保存函数接口之前,所有的转换过程都省略掉,有效提高了达梦服务器的启动速度和执行速度,基本做到与Windows环境下的性能相当。唯一美中不足的地方在于:在达梦服务器运行期间,手工输入命令调用write_leak_to_file函数接口保存信息结点到文件时,第一次调用过程中转换的时间要花费10分钟左右,后续的调用时间虽然明显减少,但仍然不能满足我们使用要求。

  优化方式三:以空间换取时间

  最后,仔细分析一下从函数内存地址转换到函数名称和源代码定位行数的过程,我们可以发现,虽然达梦服务器在执行的过程中分配内存的次数可能非常巨大——这也意味着前面的程序进行堆栈地址信息转换的次数也非常巨大,但是,达梦服务器的函数总个数是非常有限的,也即函数内存地址的总数量是非常有限的。那么,我们完全可以在内存中建立一个函数内存地址与具体函数信息的映射链表,这样就不需要每次都调用效率低下的system函数来执行addr2line转换命令,而是直接在映射链表中寻找已有结果,只有在映射链表中不存在对应的函数内存地址记录时,才进行此操作,并把结果保存到映射链表中,处理速度将得到明显提高。实现的流程如下:

  1、达梦服务器调用write_leak_to_file函数接口来保存信息,遍历每一个信息结点;

  2、如果当前结点的堆栈地址信息已经转换过,则转到第5步;

  3、如果当前结点的堆栈地址信息没有经过转换,则解析每一层的函数内存地址;

  4、如果函数内存地址在映射链表中不存在,则调用system函数来执行addr2line转换,并把转换结果保存到映射链表中,避免下次重复转换;如果映射链表中存在对应地址,则直接取相关的函数名称和源代码定位行数;

  5、把转换后的信息结点保存到文件中。

  修改后重新进行测试,发现达梦服务器第一次调用write_leak_to_file函数接口时花费时间少于2秒,在前面的基础上又提高了近百倍,已经能够很好的满足我们的使用要求。可见,通过增加映射链表的管理功能,虽然占用了更多的内存,也增加了程序的复杂性,但是对于性能的提升还是很有帮助的,这就是典型的以空间开销换取时间开销的例子。

  总结

  通过上面三种优化方式,我们一步一步把程序的性能提高到了理想的程度。这给我们的教训是:仔细分析小问题有时可以带来巨大的实际好处。寻找和分析程序的性能瓶颈,以及想方设法有效避免或减少它带来的性能影响,完全可以把一个效率极其低下的程序的性能提升到理想的程度。

0
相关文章