• 首页
        • 更多产品

          客户为中心的产品管理工具

          专业的软件研发项目管理工具

          简单易用的团队知识库管理

          可量化的研发效能度量工具

          测试用例维护与计划执行

          以团队为中心的协作沟通

          研发工作流自动化工具

          账号认证与安全管理工具

          Why PingCode
          为什么选择 PingCode ?

          6000+企业信赖之选,为研发团队降本增效

        • 行业解决方案
          先进制造(即将上线)
        • 解决方案1
        • 解决方案2
  • Jira替代方案
目录

既然有了tcmalloc或者ptmalloc这样的库,为什么还要自己写内存池呢

有了tcmalloc或者ptmalloc这样的库,还要自己写内存池的原因:1、可以定制化;2、内存分配效率更高;3、可以减少锁的争用;4、可以更好的控制。可以定制化是指自己编写内存池可以满足更加个性化的需求。

一、有了tcmalloc或者ptmalloc这样的库,还要自己写内存池的原因

1、可以定制化

自己编写内存池可以满足更加个性化的需求,有效降低内存使用,避免内存碎片问题。通常一个应用程序拥有自己独特的内存使用模式,自己编写内存池可以为特定的应用程序场景量身定制。

2、内存分配效率更高

自己编写内存池可以直接申请一大块内存空间,有效减少申请内存的次数和时间。内存池中的内存可以被重复利用,降低内存申请和释放造成的成本。

3、可以减少锁的争用

在多线程环境下,TCMalloc和ptmalloc库的内部实现会使用同步机制来保证并发访问的正确性,会造成锁的争用现象。自己编写内存池可以减少同步机制的使用,提高访问速度。

4、可以更好的控制

自己编写的内存池可以比库更好地控制内存分配和释放的行为,例如在某一个时间停止分配内存等操作。

二、tcmalloc介绍

1、简介

TCMalloc 是 Google 对 C 的 malloc() 和 C++ 的 operator new 的自定义实现,用于在我们的 C 和 C++ 代码中进行内存分配。 TCMalloc 是一种快速、多线程的 malloc 实现。

​ TCMalloc为每个线程分配了缓存,这个缓存是线程私有的,可以减少多线程程序竞争。对于小对象的内存分配,首先会去请求线程缓存,不用加锁,如果缓存不能满足的话,需要去向后面的内存存储结构中获取,此时需要加锁获取,因为其他线程可能正在获取内存空间,但是大部分情况下线程缓存就可以满足内存请求,所以几乎不需要锁。对于大对象的内存分配,TCMalloc尝试着使用细粒度和高效的自旋锁。另外一个TCMalloc的好处是小对象内存分配效率高。例如,分配n个8 byte的对象时,使用大约8n * 1.01byte的空间,只有百分之一的空间浪费。ptmalloc2分配内存的方法为每个对象使用一个4 byte的标头,并且将大小四舍五入为8 byte的倍数,最终使用16n byte。

2、TCMalloc架构

我们可以将TCMlloc分为三部分:front-end;middle-end;back-end。 它们的职责分别是:

  • Front-end:是一个缓存,提供内存快速分配和重分配内存给应用程序的功能。它主要有2部分组成:Per-thread cache和Per-CPU cache。
  • Middle-end:负责给front-end提供缓存。当front-end缓存不足时,首先从middle-end中获取。它由Central free list组成。
  • Back-end:负责从系统获取内存。当middle-end中的内存不足时,从back-end中获取。它主要设计page heap的内容。

3、小对象和大对象分配

TCMalloc维护了一份空间大小映射表,当分配小对象内存空间时,会从这个表里寻找合适大小的内存,点这里能都看到。例如,12字节的分配将会寻找16字节大小的内存空间。空间大小级别是为了在向上取最小满足的内存空间时减少浪费。

​ 如果分配的内存空间大于1MB,那么直接从后端分配,因此,大对象内存空间不会缓存在front-end和middle-end中。大对象分配时会向上取最小满足页大小的内存空间。

Middle-end负责为 Front-end提供内存以及把多余的内存放回Back-end。Middle-end是由Transfer cache和Central free list组成。尽管Transfer cache和Central free list经常被认为是一个东西,但它们是有区别的。当Front-end访问Middle-end时需要先加锁然后再获取内存,这会造成线性访问的时间消耗。

三、内存池介绍

1、基本概念

(Memory Pool)是一种内存分配方式,又被称为固定大小区块规划(fixed-size-blocks allocation)。通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。

内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。在内核中有不少地方内存分配不允许失败。作为一个在这些情况下确保分配的方式,内核开发者创建了一个已知为内存池(或者是“mempool”)的抽象。一个内存池真实地只是一类后备缓存,它尽力一直保持一个空闲内存列表给紧急时使用。

2、实现示例

内存池的实现有很多,性能和适用性也不相同,以下是一种较简单的C++实现—GenericMP模板类。在这个例子中,使用了模板以适应不同对象的内存需求,内存池中的内存块则是以基于链表的结构进行组织。GenericMP模板类定义:

template <class T, int BLOCK_NUM= 50>
class GenericMP
{
public:
static VOID *operator new(size_t allocLen)
{
assert(sizeof(T) == allocLen);
if(!m_NewPointer)
MyAlloc();
UCHAR *rp = m_NewPointer;
m_NewPointer = *reinterpret_cast<UCHAR**>(rp); //由于头4个字节被“强行”解释为指向下一内存块的指针,这里m_NewPointer就指向了下一个内存块,以备下次分配使用。
return rp;
}
static VOID operator delete(VOID *dp)
{
*reinterpret_cast<UCHAR**>(dp) = m_NewPointer;
m_NewPointer = static_cast<UCHAR*>(dp);
}
private:
static VOID MyAlloc()
{
m_NewPointer = new UCHAR[sizeof(T) * BLOCK_NUM];
UCHAR **cur = reinterpret_cast<UCHAR**>(m_NewPointer); //强制转型为双指针,这将改变每个内存块头4个字节的含义。
UCHAR *next = m_NewPointer;
for(INT i = 0; i < BLOCK_NUM-1; i++)
{
next += sizeof(T);
*cur = next;
cur = reinterpret_cast<UCHAR**>(next); //这样,所分配的每个内存块的头4个字节就被“强行“解释为指向下一个内存块的指针, 即形成了内存块的链表结构。
}
*cur = 0;
}
static UCHAR *m_NewPointer;
protected:
~GenericMP()
{
}
};
template<class T, int BLOCK_NUM >
UCHAR *GenericMP<T, BLOCK_NUM >::m_NewPointer;
GenericMP模板类应用
class ExpMP : public GenericMP<ExpMP>
{
BYTE a[1024];
};
int _tmain(int argc, _TCHAR* argv[])
{
ExpMP *aMP = new ExpMP();
delete aMP;
}

延伸阅读1:ptmalloc简介

ptmalloc是glibc默认的内存管理器。我们常用的malloc和free就是由ptmalloc内存管理器提供的基础内存分配函数。ptmalloc有点像我们自己写的内存池,当我们通过malloc或者free函数来申请和释放内存的时候,ptmalloc会将这些内存管理起来,并且通过一些策略来判断是否需要回收给操作系统。

相关文章