本文主要讲的是 各个C++版本 中的STL标准库中的默认内存分配器 的 各种实现
VC6 标准分配器实现
VC6 它什么内存管理都没做 直接调用的 operator new
而 operator new
内部实际上 调用的就是 malloc
此外 VC6 下 是以元素为单位 比如 int 的话 他就会分配 512 ints 如果是 double 则会分配 512 doubles
而 GNU-C++ 2.9
以字节为单位 GNU-C++ 4.9
以元素为单位 在 G2.9 中的容器默认分配器 被移到了 pool_allocator
中
注意 G2.9 的标准分配器 和 容器默认分配器 不一样噢!
GNU-C++ 2.9 标准分配器实现
std::allocator
标准分配器 也是什么都没做
但是 G2.9
容器使用的分配器 不是上面的那个 std::allocator
而是 std::alloc
GNU-C++ 4.9 标准分配器实现
也是什么都没做… 没有任何的特殊设计
G4.9
里有很多扩展的分配器 其中 G2.9
中的 std::alloc
变成了 __pool_alloc
因此 用 vector<string, __gnu_cxx::__pool_alloc<string>> vec;
能够节约不少 内存分配的内碎片 因为之前说过 malloc()
分配出来的内存 是有记录 当前区块大小的开销
GNU-C++ 2.9 std::alloc 原理
首先维护 16条自由链表 free_list[16]
掌控着16种不同小内存的分配 如果 超出了这16种小内存即 超过128字节的时候 则 交给 malloc()
去分配
16种小内存 分别为 8的倍数 如果小于8的倍数 会将其调整为 8 的倍数的边界
当 外界使用 std::alloc
分配内存时 每次 std::alloc
会获取 20 * 2
个 size 那么大的大内存块 其中 一份 20
进行切割为一个单向链表 剩下一份 20
会保留下来 以便下次使用
1 | 比如 我向 std::alloc 分配 32 字节的内存 |
换句话说 就是 如果没有预备内存的话 就会去申请 每次申请 20 * 2 * size + [RoundUp(目前已申请的量 >> 4)] 追加量
那么大的内存块 下次申请 如果有预备内存就直接用保留下来的那一份 如果没有了 则再次按照一开始的设定去申请一整块大内存
RoundUp()
函数内部是将其调整为 8
的字节
此外 就算预备内存足够大 也不会切超出 1~20块内存 永远在 20 以内
当系统内存不够用
系统内存不够用了 就从所需内存的右侧链表找 如果找的到 就直接将其一块小区块 注入 内存池中 再将其分配出去
如果所有的右侧都没有了 那就回到一级适配器 给 new handler 处理 看看还有没有一些补救措施 释放掉一些内存给我们用
如果还是不行 那就 凉
了
第一级配置器
当第二级配置器失败了 就交给 第一级配置器 模拟 new handler
不断循环 想办法挖出更多的内存
但是 C++ 本身就有这种机制 GUN-C++ 4.9
不再有这段了
第二级配置器
第二级配置器就是我们上面讲的那个过程的配置器
总结 G2.9
这套内存分配器 通过维护一个内存池和16个自由链表 来减少 malloc 的调用 从而减少 malloc 时所浪费掉的内存(用于记录内存块大小)
可能有的人会觉得 就占用这一点内存没什么 但是 如果大批量的 malloc 小内存的话 那记录内存块大小的消耗就非常可观了.
但是也有缺陷 这一套方案 并没有 free 掉自己所占有的内存 而是将其存回自己的自由链表中 一方面是 很难 free 因为链表到最后全都乱掉了 只有地址相邻 大小相等 才能合并起来 最后再还回去.
因此 这可能也是为什么 G4.9
的时候 容器的标准内存分配器 不用这一套的原因 而是 采取什么都不做的形式 来分配内存.
此外 我们知道 当内存池 或者 自由链表的区块不够了 就会去 malloc 但是 每次 malloc 都是取 20 * 2 * size + 递增量
那为什么不每次 除以 2 想尽办法把 系统内存全部扣光呢? 可能是为了 把那些内存让给其他进程吧..
代码实现可见 Memory_Pool