年初在维护一个http server程序的时候,发现5u下性能比4u下降了一倍不止。最后通过程序埋点的方法确认,问题出在calloc调用上。程序上有一段逻辑,每来一个链接,通过calloc申请一块2M的空间,在5u下calloc性能下降厉害。当时通过malloc+字段实例化的方式替换了calloc,解决了这个问题,没有太往下纠结。
今天收到封邮件,也是程序从4u升级到5u下,性能下降严重,最后定位到也是calloc的问题;但他们更进一步,分析了为啥calloc性能会下降,很不错,这里分享下。
大致的意思是:calloc性能下降是由于glibc版本不同引起的,在5u下内存分配的策略和4u不同。内存分配有两种方式:brk调用,或mmap调用;对于小空间一般在brk中分配,大空间通过mmap分配。在4u中,大小空间的阀值是定死的128K,在5u下这个阀值是动态调整的,最大可以到32M。由于分配的都是大空间,由于这个策略调整,在4u下calloc空间是通过mmap获得的,5u下是通过brk获得的。mmap分配的空间有一个特点:不发生读写操作是不会分配实际物理内存的;加载内存时,系统会把page全部清零。所以在4u下,calloc分配大内存的效率跟malloc效率差不多;而在5u下,由于在brk中分配,必须要做一次清零操作;这就是问题了。
从应用上,建议不要使用calloc函数,而改用malloc+实例化。
先简要总结一下速度降低的原因:
1. 4u和5u的glibc版本不同,分别是2.3.4和2.5.
2. glibc中内存分配使用的是ptmalloc,ptmalloc在2006年有一个改动,体现到了glibc2.5中,以前(glibc2.3.4)触发mmap的阈值是固定的,是128K,超过128K的内存申请都是通过mmap申请的,小于128k的内存申请使用brk;这次改动(glibc2.5)对于mmap的使用是动态调整阈值的,默认是128K,但是如果有超过128K的内存通过mmap申请、释放,那么触发mmap的阈值会被调整,有可能会变大。 这个调整的原因是因为mmap的开销比较大,而且现在内存也越 来越便宜,频繁分配临时内存也比以前大,所以系统更倾向于使用brk来降低开销。如果用户申请了大内 存又释放了,那么系统认为,可能这个程序对大内存的申请和释放是平常的事情,所以会把mmap的阈值调大,减小mmap的调用次数,改用开销更小的brk。
3. malloc不负责将申请的内存区域清零,但calloc负责。
4. 内核在分配匿名页时为了保证安全,不会被这个进程读到其他进程已经释放的空间,会做清零的工作。但如果分配的不是匿名页,比如是由mmap文件导致的分配页,不会清零。对 于mmap申请的匿名页内存的清零,是发生 在缺页时,由操作系统保证。brk分配的内存,需要calloc自己清零。
5. 如果没有写内存,只是mmap申请了一块虚拟内存,不会发生缺 页,没有分配物理内存,也不会引起清零的操作。一般操作系统中4k为1页。
6. pidmatch由于逻辑调整,以前每次调用需要 申请一块超过128k的内存,且通过calloc调用。但现在实际上不需要那么大 内存,申请了也没有写那么多页。所以在glibc2.3.4下,通过mmap申请内存,但没有触发很多缺页, 所以其实很多清零的操作也没有做,而在glibc2.5下 面,触发mmap的阈值会自动变大,通过brk来分配内存,无论是否写了内存, 都会做清零操作,所以导致模块变大,性能下降。本来是ptmalloc一个改进的设计,但由于程序使用 不当,优点倒成了缺点。
7. 改进方法:由于模块实际不需要那么多内存,且现在也不需要清零,所以将申请内存改小,从使用calloc改为使用malloc,性能恢复到4u的状态。