文章目录

  • 栈内存空间是否够用
  • 系统调用申请内存
  • 最简单的内存分配器实现 -- bump allocator
  • 可扩容的 Bump alloactor
  • 通过free-list 管理的 allocator
  • 通过size-buckets 维护多个free-list 的 allocator
  • Cache friendly allocator
  • 需要考虑更多问题的allocator
    • 性能
    • 易用性

本文希望描述一个基本的内存分配器的设计演进,包括基于ptmalloc 实现的glibc-malloc ,jemalloc 以及 tcmalloc 为什么会被设计成如今的样子(他们的基本形态都是非常接近的,无非tcmalloc 比 jemalloc和glibc-malloc 多了一些内存高效利用上的设计)

在没有内存分配器的时候,我们os系统最开始的时候拥有自己的栈空间,用来保存函数栈以及内部的变量,大家先抛开我们习惯用的malloc/free 以及 new/delete,来整体看看内存分配器的演进过程,本文更多的是一些设计上的思考。

栈内存空间是否够用

如下代码提就是一个栈空间的基本数据存储。

#include <stdio.h>int main() {int a = 10;int b = 20;int c = a + b;return 0;
}

在编译生成的汇编代码中,我们可以看到对于变量 a 和 b的处理,其所占用的内存大小已经被分配好。10这个数值被填充到了代表a的栈底指针减去4字节之后的地址-4%(rbp),同理20 被填充到了栈底指针减去 8字节之后的地址-8(%rbp)

可以看到,编译期间,变量a,b 以及最后的c 都已经明确了具体的存储空间的大小以及存储对应的虚拟地址。

        .file   "stack.c".text.globl  main.type   main, @function
main:
.LFB0:.cfi_startprocpushq   %rbp # 入栈.cfi_def_cfa_offset 16.cfi_offset 6, -16movq    %rsp, %rbp  # 添加栈底指针.cfi_def_cfa_register 6movl    $10, -4(%rbp) # 变量amovl    $20, -8(%rbp) # 变量bmovl    -4(%rbp), %edxmovl    -8(%rbp), %eaxaddl    %edx, %eaxmovl    %eax, -12(%rbp) # 变量cmovl    $0, %eaxpopq    %rbp # 出栈.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE0:.size   main, .-main.ident  "GCC: (GNU) 7.3.1 20180303 (Red Hat 7.3.1-5)".section        .note.GNU-stack,"",@progbits

栈内存分配基本优劣如下:

优势:

  • Automacitcally handled by the compiler,编译器会编译期间分配好对应的栈内存
  • very fast allocation 拥有对应的cpu指令来专门处理栈内存的分配
  • very fast cleanup 释放的时候也拥有对应的指令(函数栈弹栈)

劣势:

  • fixed sizes,内存大小需要是已知的,内存大小有限,比如x86_64 系统下默认的栈空间只有10M(ulimit -s)
  • fixed lifetimes : 作用域仅仅被限制在了当前函数内部。比如,想要在一个函数内部创建一个链表并返回,是完全不可能的。

栈内存对于我们应用程序的使用场景来说显然还是不够,那有没有可以直接分配内存的系统调用?

系统调用申请内存

mmap : 设置内存protection(mmap的内存区域可以被当前进程的其他程序更改),频繁的中断 以及 会陷入内核;释放的时候unmap即可。

Mmap 解决了 栈方式分配内存的 fixed-sizes 的问题,用户可以不限制内存大小来分配内存。

  1. 整体非常慢,频繁得触发page fault 中断并使当前进程陷入内核。
  2. 浪费内存较为严重。因为申请内存的粒度只能是4K的page,在一些应用场景中对内存浪费较为严重(申请8byte的区域,创建一个链表节点,需要消耗4k的内存)。

Mmap 并不能作为构造一个高效可靠的内存分配器的雏形,接下来还需要做更多的工作来对内存进行管理。

那我们想像这样一个简单的内存分配器,就是先模拟栈空间进行内存分配,取一个定长的内存区域被用来存储数据,分配的过程可以根据用户的需求来分配。

最简单的内存分配器实现 – bump allocator

bump allocator,类似一个用户栈空间(不用 os 自己的进程栈空间)。

用户申请一个内存区域,则将申请的这个内存对象入栈。这个栈空间管理的内存能够控制变量的生存周期,只有当你从栈中移除这个内存对象的时候,这个内存对象的生命周期才算结束。

优势:

  • 高性能,fast allocation
  • 变量的存储生命周期可控

缺点:

  • fixed total memory,需要开辟一段固定大小的内存。
  • cannot free memory: 这个含义其实是说,因为使用的是栈空间管理的内存,想要释放最开始申请额8 bytes 的存储空间,需要先释放掉在 8之上的其他内存对象,这个时候其他的内存对象对于应用程序来说并不一定想要释放。

问题很明显,还是和传统的函数栈空间一样 fixed total memory 以及 无法快速释放内存。

可扩容的 Bump alloactor

那我们先来通过expandable memory 来解决 fixed total memory的问题。

当用户程序耗尽了第一次申请的内存区域,则分配一个新的 内存区域用于用户申请(内存管理的arena机制),已经满的内存区域则保留,直到用户想要从栈顶释放内存。

优势:

  • Fast allocation
  • expandable total memory
  • manual lifetime

劣势:

  • cannot free memory,仍然无法有效释放内存,需要先弹栈,才能找到需要释放的内存对象。

通过free-list 管理的 allocator

free-list,通过一些元数据来管理内存空间,比如使用链表,将应用程序申请的内存对象通过链表串联起来, 如果用户想要释放一个变量,则释放这个变量对应内存的单链表节点即可。

现在free memory终于可以随意进行了,能够随意申请,随意释放,不会受内存大小的限制,这样看起来功能确实没有问题了。

可是我们还需要性能 以及 成本,,,

劣势:

  • allocations have minium sizes,一个链表指针在64位系统下需要8byte的存储空间,表示每次申请至少需要8byte的存储空间 管理free-list 的节点。
  • very slow (释放的时候,可以随意释放任何一个节点,只是需要从头顺序扫描整个链表,代价很高)
  • memory fragmentation 内存碎片,整个内存空间在大内存和小内存对象混合在一起的时候无法有效管理小内存。比如一个链表节点大小最小是8bytes,用户申请了一个1byte 和 一个 7bytes的存储空间,这个时候需要至少16bytes的存储,1byte 对应的存储区域会有一个7bytes的内存碎片一直无法被使用。

这样的性能 以及 对成本这样的浪费,简直不能忍,继续向下看。

通过size-buckets 维护多个free-list 的 allocator

每一个size(不太可能每一个size 一个free-list,不易维护) 或者 一批 size 维护一个自己的free-list,这样当前free-list 只需要存储大小接近的内存对象就可以了,内存碎片问题会被极大的降低。

且释放内存的时候不需要扫描整个混合在一起的大链表,扫描被均分到不同的size buckets。

优势:

  • 在前面的基础上能够正常释放内存
  • 解决了扫描慢的问题(释放时不需要扫描整个大链表,只需要扫描对应的size-buckets 的链表即可)
  • 内存碎片问题(通过按照size 来划分free list,有效得降低了内存碎片)

劣势:

  • Cache pressure ;在高并发下分配内存的过程中频繁的cache-miss 会导致cpu 的负载较高。cpu 需要频繁得在不同的size-buckets 下进行切换,无法保持良好的cache命中(每次遍历都需要重新load 内存到cpu-cache,链表的访问本身是随机访问,加上不同的size-buckets 的访问,都会导致cpu cache-miss较高)。

Cache friendly allocator

slab allocator,每一个size维护一个slab,这个 slab只需要管理当前size 的内存分配即可。

解决了cpu 对链表数据的随机访问的影响, 每一个slab 内部使用数组来管理内存对象,数组对cpu-cache 更友好。

每一个slab 能够管理一批连续的内存空间,内存在slab上的分布连续性较强。

这个连续性肯定是相比于链表的随机访问来说的,在slab上尽可能连续的分配当前size,释放的时候能够尽可能得降低cache miss,减少cpu-cache 的压力。

到此,我们有了一个功能齐全,性能还不错的内存分配器的概要设计,但这只是单线程场景的内存分配器。想要实现一个工业级的内存分配器,还有更多的问题需要去考虑。

需要考虑更多问题的allocator

性能

  • Alignment。尽可能对齐cache-line,来保证cpu 访问的性能
  • Multi-Thread / Multi-core 下的性能,比如jemalloc 和 tcmalloc 设计的 thread-cache / per-cpu cache等

易用性

  • C compatibility – 兼容c语言的malloc/free等,想要支持 C++,这需要支持 operator new/delete
  • Debugging,需要能够暴露内部状态,方便debug或者用户查看内存占用情况
  • Binary size – 运行库文件不能过大,它只是一个内存分配库

这也就是jemalloc/tcmalloc/glibc-malloc 为什么如此通用的原因,因为其实现过程是在功能/性能/成本之间的trade-off,且这个trade-off 达到了极致,被工业界认可。

内存分配器设计的演进相关推荐

  1. 内存分配器 (Memory Allocator)

    对于大多数开发者而言,系统的内存分配就是一个黑盒子,就是几个API的调用.有你就给我,没有我就想别的办法.来UC前,我就是这样认为的.实际深入进去时,才发现这个领域里也是百家争鸣,非常热闹.有操作系统 ...

  2. 内存分配器(Memory Allocator)

    原文链接 : https://yq.aliyun.com/articles/254033 对于大多数开发者而言,系统的内存分配就是一个黑盒子,就是几个API的调用.有你就给我,没有我就想别的办法.来U ...

  3. mysql 自动管理内存_MySQL内存管理,内存分配器和操作系统

    导读 作者:Sveta Smirnova 翻译:郑志江 校对:徐晨亮 原文 :MySQL Memory Management, Memory Allocators and Operating Syst ...

  4. MySQL内存管理,内存分配器和操作系统

    原文 :MySQL Memory Management, Memory Allocators and Operating System 本文涉及链接在文末展示 When users experienc ...

  5. Linux glibc内存管理:用户态内存分配器——ptmalloc实现原理

    文章目录 ptmalloc 设计假设 Arena Chunk Bins 内存分配.释放流程 总结 C++ STL : SGI-STL空间配置器源码剖析 Linux 内存管理 | 物理内存管理:物理内存 ...

  6. 自己动手实现一个malloc内存分配器 | 30图

    对内存分配器透彻理解是编程高手的标志之一. 如果你不能理解malloc之类内存分配器实现原理的话,那你可能写不出高性能程序,写不出高性能程序就很难参与核心项目,参与不了核心项目那么很难升职加薪,很难升 ...

  7. 内存分配器ptmalloc,jemalloc,tcmalloc调研与对比

    内存分配器ptmalloc,jemalloc,tcmalloc调研与对比 rtoax 2020年12月 1. 概述 内存管理不外乎三个层面,用户程序层,C运行时库层,内核层.allocator 正是值 ...

  8. tcmalloc内存分配器分析笔记:基于gperftools-2.4

    目录 一. SizeMap 二. 线程缓存ThreadCache ThreadCache的实现 1. 内存分配 2. 内存释放 三. 中央缓存Central Cache CentralFreeList ...

  9. TCMalloc内存分配器如何减少内存碎片?

    目录 前言 什么是内存碎片? 内存碎片-Freelist 内存碎片-Segregated-Freelist 内存碎片-Buddy-System 内存分配算法的比较 TCMalloc的内存碎片 TCMa ...

最新文章

  1. 刻意练习:LeetCode实战 -- Task10. 两数相加
  2. linux-认识与分析日志
  3. shlve模块 序列化 python任意的数据
  4. linux脚本获取usb设备,Linux基于USB端口执行脚本
  5. Java容器有哪些?
  6. java pnpoly算法_C语言实现的PNPoly算法代码例子
  7. 【转发】未能加载文件或程序集“Oracle.DataAccess”或它的某一个依赖项。试图加载格式不正确的程序。...
  8. rabbitmq消费者获取消息慢_RabbitMQ:快速生产者和慢速消费者
  9. Python网络爬虫学习笔记(十):PyQuery库的使用
  10. mysql 大于小于不可在一个不等式中同时写
  11. 一起话单业务量下降问题的排查过程
  12. 了解H.264(AVC)的你,知不知道什么是SVC?
  13. 红外遥控器解码串口输出模块结合51单片机+oled屏幕实现遥控器红外解锁( STC89C52RC)
  14. 学习笔记(1):Echarts+Asp.Net+Sql Server报表开发视频教程-折线图和曲线图
  15. 手机卫星定位系统_中国北斗:已启用全球定位服务,我们如何使用手机北斗卫星导航?...
  16. 想想五年后的你,什么工作值得你去拼命? 转~ 推迟满足感 成长比成功重要
  17. php面试题目(中等水平)
  18. weblogic 打补丁
  19. 执行python manage.py migrate报错问题解决
  20. 基于STM32MP157的鸿蒙学习(一)— 资料下载及入门

热门文章

  1. bitset类型, 标准库类型
  2. JQuery EasyUI的常用组件
  3. 下载MySQL数据库
  4. Python笔记(2)函数
  5. EBS fnd_global.apps_initialize
  6. awk处理文件内容格式
  7. .Net MVC3中取得当前区域的名字(Area name)
  8. unity人物旋转移动代码_Unity3D研究院之脚本实现模型的平移与旋转(六)
  9. python测试程序的qps和响应时间代码_Python并发请求下限制QPS(每秒查询率)的实现代码...
  10. io vivado 怎么查看ps_ZYNQ 7020学习笔记之PL侧普通信号中断PS的实验