分享免费的编程资源和教程

网站首页 > 技术教程 正文

内存池的原理和应用简介(一)

goqiw 2024-11-27 14:00:37 技术教程 31 ℃ 0 评论

什么是内存池

在程序开发的过程中,我们经常会遇到线程池、连接池的概念,尤其是对服务器开发者来说,掌握这些“池”的原理和用法是一项基本功。

内存池其实和线程池连接池的概念差不多,也就是做了一层缓存,预先申请好一些内存区域,当应用层需要开辟内存(例如执行new、malloc等操作)时,直接从这些预申请的内存中切出合适的一块交给应用层,而不是每次都向操作系统去申请内存。

为什么要用内存池

为什么要做内存池呢?在之前的文章中曾经讲过,向操作系统申请内存的效率远比纯逻辑运算要差得多。在一些需要频繁申请释放内存的场合,例如做网络通信模块时,这个问题显得尤为明显。

之前的文章中也讲过malloc申请内存的大致原理,链接在这里:

Malloc、free和realloc浅析

把文中的图单独拿出来看一下:

拿C/C++来说,在频繁申请和释放内存的过程中,可能会产生大量不连续的内存碎片,不仅影响速度还会导致额外的内存浪费。

对其它带有GC功能的语言,这个过程的核心原理是类似的,如果语言的GC和编译优化策略比较成熟,例如java的话情况会好一些。如果使用的是一些解释型语言或者GC策略不够成熟的语言,还可能会产生额外的频繁GC,拖慢程序的性能。在这种情况下,内存池就应运而生了。

内存池的实现原理

大部分现代应用都会使用一些现成的框架来实现网络通信或者其他需要用到内存池的场景,不一定会用到手写内存池的情况。不过了解一些内存池的基本原理,有助于我们设计高性能的架构。

最基本的内存复用

先来看一段最简单的通过对象复用来实现性能优化的过程。

优化前的代码:

Bash
public class Test{
	int a;
	int b;
	int testFunction() {
		return a + b;
	}
}
public void test() {
	for(int i = 0; i<10000; i++) {
		Test test = new Test();
		test.a = i;
		test.b = i+1;
		print(test.testFunction());
	}
}

优化后的代码:

Bash
public class Test{
	int a;
	int b;
	int testFunction() {
		return a + b;
	}
}
public void test() {
	Test test = new Test();
	for(int i = 0; i<10000; i++) {
		test.a = i;
		test.b = i+1;
		print(test.testFunction());
	}
}

看上去很简单,很基础,只是把创建对象申请内存的语句提到了循环的外面,来避免频繁的内存操作,提升程序性能。在常见的C++、java和C#的编译器中,编译器其实会自动帮开发者做这种最初级的编译优化,对这些开发者来说,写在里面外面都是一样的。

然而,在当下的手机游戏行业中,有一种常见做法是使用编译型语言做底层渲染,解释型语言做业务逻辑。在这种情况下,有时就需要开发者关注一下这方面的问题。

例如cocos2dx这款引擎,在它以前的版本中,使用过js来做业务逻辑。在我当年使用的cocos2dx版本中,编译时对js脚本的优化基本上是没有的,因此在进行大量循环的时候,把new提出来的性能提升是相当明显的。听说cocos2dx现在改用ts做脚本了,我对ts了解不多,不知现在的编译过程是否会对脚本做编译优化。

这种基本的对象复用,其实就是内存池设计的最本质理念:提前申请内存,以后做复用。再复杂的内存池实现,其核心理念也不外如是。

Google的TCMalloc

TCMalloc的全称是Thread-Caching Malloc,即线程缓存的malloc,是google开发的一套基于C++的内存管理系统,用来替代C++开发中直接使用malloc和free,在频繁分配对象的时候速度比malloc快得多。Google在实现Go语言的过程中,就使用了TCMalloc来实现GC策略。

在前面的文章中,我们介绍过malloc中最基础的空闲链表的概念,TCMalloc将空闲链表做了进一步的扩展。

在TCMalloc中,按照空闲块的尺寸维护了多级的空闲链表。下面是第一级链表的模型:

当应用层申请较小的空间时,TCMalloc会从最小的满足条件的空闲块中取出一块分配给应用层。例如申请7字节空间时,TCMalloc会从8字节的空闲链表中取出一块,申请一个17字节的空间时,从32字节的空闲链表中取出一块。

这种小空间缓存是基于线程的,多线程使用的thread cache是各自独立的,因此整个分配过程无锁,速度较快。瑕疵也很明显,就是存在一定的冗余量,不过这点冗余相对速度来说是可以接受的。

上图的结构在TCMalloc中称为一个页(page),是线程级别的小对象缓存。当要分配的对象尺寸大于一个page时,会进入第二级管理,使用相邻的多个page,组成一个page集,在TCMalloc中这种由多个连续page组成的集合称为一个span。

Span的管理和page其实也是类似的,有1页大小的span列表、2页大小的span列表、N页大小的span列表……在span释放后,也会进行回收合并,将相邻的小span组成一个大的span。TCMalloc使用radix tree的结构对span和page进行索引。

当应用层向TCMalloc申请空间时,TCMalloc的流程大致是这个样子的:

1、如果是大对象,由全局的Central Cache直接为应用层分配span。如果是小对象,则进入Thread Cache流程。

2、当Thread Cache存在满足条件的空闲块时,直接返回空闲块。

3、如果Thread Cache不存在或剩余内存不够,就由Central Cache从自己管理的span中扯出一块来丢给Thread Cache。

4、如果Central Cache家里也没有余粮了,就使用sbrk或者mmap向操作系统要一块。

从这个流程就可以看出,TCMalloc为什么叫TCMalloc,因为它性能表现最好的就是无锁的线程系别的小对象缓存。当应用程序基于C++开发,有频繁申请释放小空间需求时, TCMalloc是一个不错的选择。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表