性能优化-放开那片内存,让我来!
转载请注明以下内容:
来源:公众号【编程珠玑】
作者:守望先生
ID:shouwangxiansheng
性能优化是一个常有的事情,通常来说
不要过早优化-当你没有性能问题时,不需要过早考虑优化,当然对于一些代价很小,收益却很大的手段可以考虑做进来,例如最常见的就是根据业务需求选择合适的数据结构。
不要过度优化。优化都是有目标的,比如你需要达到多少TPS,那么你按照这个目标去优化即可,有些优化虽然能否提升性能,但可能对代码的可维护性造成破坏。
本人对此没有过多涉猎,仅分享工作中接触到的一些内存。
内存性能问题
有很多方面会造成性能问题,例如:
业务流程设计不合理,导致很多没有必要的计算
数据结构选择不合适
缓存使用不当
示例
假设你已经者使用profiler分析,已经发现内存分配是性能瓶颈:
// 来源:公众号【编程珠玑】
// 作者:守望先生
// malloc.cc
#include
#include
#include
#include
void GetMemory(){
for(int i = 0;i < 100000000; i++){
void *p = malloc(1024);
if(NULL != p){
free(p);
p = NULL;
}
}
}
int main(){
std::vector<std::thread> th;
int nr_threads = 10;
for (int i = 0; i < nr_threads; ++i) {
th.push_back(std::thread(GetMemory));
}
for(auto &t : th){
t.join();
}
return 0;
}
代码非常简单,仅仅是不断分配内存而已。
编译并尝试分配十亿次:
$ g++ -g -o malloc malloc.cc -lpthread
$ time ./malloc
real 0m8.677s
user 0m29.409s
sys 0m0.029s
分配十亿次内存,使用时间大概17s左右。另外一个终端使用perf查看情况:
$ perf top -p `pidof malloc`
52.92% libc-2.27.so [.] cfree@GLIBC_2.2.5
31.94% libc-2.27.so [.] malloc
8.82% malloc [.] GetMemory
3.45% malloc [.] free@plt
2.51% malloc [.] malloc@plt
0.03% [kernel] [k] prepare_exit_to_usermode
0.01% [kernel] [k] psi_task_change
0.01% [kernel] [k] native_irq_return_iret
0.01% [kernel] [k] __update_load_avg_cfs_rq
0.01% [kernel] [k] __update_load_avg_se
0.01% [kernel] [k] update_curr
0.01% [kernel] [k] native_write_msr
0.01% [kernel] [k] __schedule
0.01% [kernel] [k] native_read_msr
0.01% [kernel] [k] read_tsc
0.01% [kernel] [k] interrupt_entry
0.01% [kernel] [k] update_load_avg
0.01% [kernel] [k] swapgs_restore_regs_and_return_to_usermode
0.01% [kernel] [k] reweight_entity
0.01% [kernel] [k] switch_fpu_return
0.01% [kernel] [k] perf_event_task_tick
从结果可以看到,大部分CPU耗费在了内存的申请和释放。
怎么办呢?第一要考虑的做法不是如何提升它,而是它能否避免?比如内存复用?而非反复申请?
比如使用内存池?但是要自己写一个稳定的内存池又需要耗费很大的精力了。怎么办呢?
性能更好的库
实际上这就引出了性能优化的一种常见方法-使用性能更好的库。那么在内存分配方面,有更好的库吗?自己又不能写出一个比libc还厉害的库,就只能用用开源的库,才能维持得了写代码的生活。
目前常见的性能比较好的内存分配库有
tcmalloc-谷歌开发的内存分配库
jemalloc
在自己编译使用redis的时候,其实你能看到它们的身影:
# Backwards compatibility for selecting an allocator
ifeq ($(USE_TCMALLOC),yes)
MALLOC=tcmalloc
endif
ifeq ($(USE_TCMALLOC_MINIMAL),yes)
MALLOC=tcmalloc_minimal
endif
ifeq ($(USE_JEMALLOC),yes)
MALLOC=jemalloc
endif
ifeq ($(USE_JEMALLOC),no)
MALLOC=libc
endif
如何使用
这里以tcmalloc为例,看一下如何使用该库替换libc中的malloc。tcmalloc使用了thread cache,小块的内存分配都可以从cache中分配。多线程分配内存的情况下,可以减少锁竞争。
获取
你可以通过源码编译获取,github地址:https://github.com/google/tcmalloc.git
不过它需要使用bazel进行构建编译,有兴趣的可以自行尝试。
也可以直接安装:
$ apt-get install -y libtcmalloc-minimal4
安装位置查看:
$ ldconfig -p | grep tcmalloc
libtcmalloc_minimal_debug.so.4 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libtcmalloc_minimal_debug.so.4
libtcmalloc_minimal.so.4 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libtcmalloc_minimal.so.4
libtcmalloc_debug.so.4 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libtcmalloc_debug.so.4
libtcmalloc_and_profiler.so.4 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libtcmalloc_and_profiler.so.4
libtcmalloc.so.4 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libtcmalloc.so.4
LD_PRELOAD
这种方式在自己测试的时候非常方便,只需要:
$ export LD_PRELOAD=/path/to/tcmalloc.so
导入环境变量,指定库路径即可。注意这里的/path/to更换成你的tcmalloc实际的路径。运行的时候,tcmalloc库就会被首先被使用了。
直接链接
这种方法就和普通库的使用没有什么区别了,链接使用就完事了。
效果
我们使用新的库,再进行10亿次的内存分配试试:
$ time ./malloc
real 0m7.152s
user 0m27.997s
sys 0m0.032s
可以看到要使用的时间少了些。当然,这里的对比严格来说不是很严谨,甚至可以说起不到对比的作用。首先这里内存分配大小比较单一,并且仅有内存分配,而没有其他处理,真正是否有效果,还是要根据实际业务程序的情况来判断。当然,整体来说,tcmalloc的效果要比libc的malloc分配内存要高效。
总结
当你的程序中存在大量的内存分配(例如C++频繁使用string),那么可以考虑使用性能更好的内存分配库了。关于tcmalloc,jemalloc等内存分配库的对比有很多,这里有兴趣的可自行了解。
推荐阅读:
5T技术资源大放送!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,单片机,树莓派,等等。在公众号内回复「1024」,即可免费获取!!