数据积压导致的程序崩溃:不要在QByteArray中储存过多的数据
如果妳不想把这篇文章读完,那么只需读这句话:
不要向QByteArray里存储过多的数据。
在本座的P2P代理项目中应用了自适应的霍夫曼压缩,为此需要统计自己发送了哪些数据,以便修正霍夫曼树模型,得到更好的压缩效果。
在实际实现的代码中,把发送出去的每一笔数据都附加到一个QByteArray的末尾,而在真正统计时从这个QByteArray的开头取出数据。这样,这个QByteArray就相当于是一个统计数据的缓存队列了。在一开始的代码中,为了避免这个部分的功能影响到其它部分的性能,将统计频率设得非常小,仅仅是64KB/10min。这个参数为后来测试中发现的一些问题埋下了伏笔。
在一次性能优化之后,本座要测试一下性能优化的成果,于是让局域网内的两个节点之间传输数据。传输的是一个1.7GB的大文件,设定的传输速度是1MB/s。在测试中,一切看着都狠好,传输速率达到了设定的1MB/s,但是在传输了大约1GB的数据之后,数据的发送方程序崩溃了。多次测试都是这样。在程序崩溃时,QT报告说底层抛出了std::bad_alloc的异常,首先确定是内存相关的问题了。
std::bad_alloc一般都是内存泄漏导致最终内存不足而产生的。本座在不久前已经为程序做过内存泄漏的检查并且修复了查出的所有问题,所以不大可能是这个原因引起的。不管怎样先确认一次再说。于是用valgrind运行一次,在传输了300MB的数据之后,有泄漏的话也该泄漏了吧,结果发现valgrind连一条错误信息都没输出,这说明程序里面确实没有内存泄漏的。
可能是系统内存不足,拒绝了程序的内存分配请求。为了验证,本座再次测试了一次,这次用系统监视器观察内存的使用情况。
在程序崩溃之前,它使用了刚刚多过1G的内存。这个时候,系统里面所有使用的内存数加起来是3.5G,而本座给电脑安装了8G的内存,所以系统的内存在这个时候还是足够的。另外,在同时VirtualBox占用了1.7G内存,也没有崩溃,所以也不是程序用完了自己的堆内存而导致崩溃的。
所以也不是内存不足引起的。
这个原因是在散步时突然想到的。
本座突然想到,既然不是内存泄漏,那么就是数据积压在某个地方了,整个程序里面可能积压数据的地方就是前面所说的用来将数据排队等待统计的QByteArray了。联想到QByteArray的API:const char * QByteArray:: constData () const,这个方法会返回一个指针,指向的是以标准C语言的方式储存了这个QByteArray中的数据的字符串。C语言中的标准字符串是在内存空间中连续存放的,那么QByteArray在内部就必须拥有一块连续的内存空间了。当向QByteArray中加入新的数据时,如果内部连续内存空间不够大,是要重新分配一块更大的空间的。当QByteArray中要储存的数据过大时,可能会再也无法在程序的堆内存里找到一块那么大的连续空间了,于是内存分配就失败了。
为了验证这个猜测,本座加了代码跟踪了这个QByteArray的长度,它的长度就能反映出它占用的连续内存空间。测试中发现,在崩溃的前夕,这个QByteArray的长度达到了650MB。这样,每当向其中加入一定量的新的数据导致超过它预先分配的内存大小时,它都会重新分配一块最少是650MB的连续内存空间,这真是难为了底层的内存管理库了。
为什么会增长到650MB呢?因为数据是以1MB/s的速度产生的,而是以64KB/10min的速度消耗的,几乎就是直线地增长了。
然后本座加上代码,限制了这个QByteArray的长度。果然,顺利地传完了1.7G的数据,并且内存占用稳定在设定的20MB。
现在已经确认是对QByteArray的使用不当造成的问题了。
不应当使用QByteArray来储存这种队列数据,这样就可以避免分配超长的连续内存空间了,程序就不会崩溃得这么快了(当然这还没根本解决问题,当程序占用的总内存超过内存寻址空间时还是会崩溃的)。使用QQueue、QList来储存这种队列数据都比QByteArray要好。
最初将统计频率设得这么低是为了避免这个部分占用过多的计算资源,影响到其它部分。经过这些时间的测试,本座估计对这些数据进行实时的统计也不会有多大影响了,这要经过实际测试来验证。不管怎样,数据要尽快消费掉,不然积累达到寻址空间的大小之后还是会使程序崩溃的。
霍夫曼编码要想取得好的数据压缩效果,必须保证统计模型能够正确反映真实数据中各个码元的概率差别。
在本座的这个程序中,设计的额定数据传输速率是256KB/s,而在这次性能测试中甚至提高到了1MB/s。但是相应的霍夫曼统计模型中的数据更新速度才是64KB/10min。这就导致霍夫曼统计模型与真实数据相差太远。难怪本座自从加上霍夫曼压缩功能以来一直没看到它起到什么明显的效果,这就是原因。
可以预期,在解决了这次碰到的问题之后,霍夫曼压缩部分的效果也会更好。
在代码中使用QByteArray的时候要先估算一下它可能需要装下多少数据,如果需要装下很多的数据,那么就要考虑是否该用别的数据结构了。像本座这样,在QByteArray里装下650MB的数据,已经算得上是对QT库的滥用了。
不要在QByteArray中装下过多的数据。
HxLauncher: Launch Android applications by voice commands