这篇文章详细介绍了高性能内存分配库的设计方案和实现思路,word 是 49 页的内容,介绍了实现高性能内存分配库需要注意的几十处关键技术和主要思路,最终加权平均性能比传统库 Google tcmalloc 库快了大约一个数量级(大约 14 倍,参见附录按照小内存更频繁,大内存更少的原则设计的测试代码默认执行的结果, 参见 test_loop() in loop 1 use nanoseconds=387419862 这一行的测试结果), 其中 4KiB 以上内存申请的性能提升较大,4KiB 的申请比 Google tcmalloc 库快了两个数量级大约 140 倍,测试方案是一个动态链接的外部接口的性能测试(so 接口开销较大,是重要影响因素之一),测试代码本身还有额外的开销(代码中有测试,但是没有扣除这块的额外开销,大约 5%),真实的性能还要更高一些。

LibLibhaisqlmalloc 库不是一个单纯以高性能为目标的内存分配库,是一个以整体优秀为目标的内存分配库。代码的健壮性,内存的低浪费和低碎片,线程的安全,数据的同步等因素,都比性能更重要,程序设计时有大量的设计目标都是以大幅度牺牲性能为代价,以放弃性能表现来换取更重要的其它方面的技术指标,实现整体更加优秀。LibLibhaisqlmalloc 库对外提供二进制的动态链接库 so 文件,动态链接库的函数接口的固有开销比较大,因此,最终性能表现大打折扣。即使这样,全新设计的 LibLibhaisqlmalloc 库的性能也比传统内存分配库有一两个数量级的性能提升,说明传统内存分配库的方案已经过时了,是即将淘汰的软件。

下面是详细内容:

第一章引言

  1. 编写目的

为了帮助用户更好地了解和使用该软件, 提高用户与软件的亲和度。该用户手册讲述怎样安装和使用高性能内存分配库,该系统使用过程中的配置,以及应注意的一些问题。

  1. 背景

a.该软件系统的名称:高性能内存分配库

b.该软件项目的任务提出者:乌鲁木齐云山云海信息技术有限责任公司

c.该软件项目的开发者:乌鲁木齐云山云海信息技术有限责任公司

d.该软件的用户(或首批用户):企业用户

  1. 参考资料

资料名称[标识符] 出 版 单 位 作 者 日 期

tcmalloc 谷歌内存分配库 谷歌

Jemalloc Facebook 内存分配库 Facebook

Ptmalloc Linux 内存分配库 Linux

第二章软件概述

  1. 描述

Libhaisqlmalloc 高性能内存分配库,简称 “高性能内存分配库”,是由乌鲁木齐云山云海信息技术有限责任公司开发和发行的内存分配库,是一种新型高性能内存分配库,管理堆内存,主要接口是 malloc 和 free,用于降低频繁分配、释放内存造成的性能损耗,并且有效地控制内存浪费和内存碎片,更适合大型应用程序的配套使用。

Libhaisqlmalloc 高性能内存分配库开发语言是 C++, 编译链工具是 gcc,软件完全自主知识产权,源码总行数高达 4.2 万行。

第三章 软硬件环境和使用方法

  1. 运行硬件环境

(1)、内存:512M 及以上(推荐 1G 以上)

(2)、CPU:主频 1 GMHz 以上(推荐 Intel Core2 以上)

  1. 运行软件环境

建议使用:Linux 操作系统 4.0 以上 64bit 版本 x86_64 GNU/Linux

对于 Linux 操作系统 3.X 64bit 版本, 可能会存在 Hugepage 无法使用的问题。

  1. 使用方法:

Libhaisqlmalloc 内存分配库的使用有两种方法:

1)设置 LD_PRELOAD = 带路径的 libhaisqlmalloc.so,

例如执行:export LD_PRELOAD=/mnt/guo/libhaisqlmalloc.so,这种方法非常简单不需重新编译程序,并且可以随时切换回操作系统默认的内存库, 或者使用其他内存库。如果需要默认自动配置,可以在配置默认环境变量配置终端启动时自动执行一些命令, 可以在 ~/.bashrc, ~/.bash_profile, ~/.profile, 或者 ~/.zshrc 文件添加这些命令。Ubuntu 操作系统修改的~/.bashrc,在最后增加一行 export LD_PRELOAD = 带路径的 libhaisqlmalloc.so。

2)将 libhaisqlmalloc 库链接到程序中,注意应该将 haisqlmalloc 库最后链接到程序中;

在 gcc 编译器的链接中使用参数 -L 路径(libhaisqlmalloc.so 所在的路径) -lhaisqlmalloc

推荐使用第一种方法,这种方式最简单,并且适应性最好,可以根据需要随时切换内存分配库。

下面是使用 Libhaisqlmalloc 库的一个范例:

以上可以看到,使用 export LD_PRELOAD 配置后,就已经自动使用 Libhaisqlmalloc 库了。

使用 Libhaisqlmalloc 库将会显示 5 行 Notice:

第 1 行:提供了版权信息,

第 2 行:提供了版本支持信息,基本版本,最大支持的 NUMA node 数量,默认为 8, 一般的机器只有一个 NUMA node,中高端服务器一般也只有 8 个以下的 NUMA node,只有 IBM 小型机以上和华为公司最高端的昆仑服务器才支持 8 个以上 NUMA node。提供了最大支持的 CPU 数量,默认为 136 个 CPU, 一般的机器均在 128 个 CPU 以下,只有高端服务器才会有 128 个以上的 CPU, 此时就需要联系软件开发商索要 ADVANCE 版本, 以便支持更多的 CPU。

第 3 行:表示这是一个未注册的测试版本,需要购买。

第 4 行:提供了一个购买合法版本的链接,购买后将提供一个 libhaisqlmalloc.conf 的配置文件,将这个配置文件拷贝到 / etc/libhaisqlmalloc.conf 下或者拷贝到当前 libhaisqlmalloc.so 文件所在的目录下均可以实现软件许可证 License 的正确配置,变为合法用户。

第 5 行:提供了 libhaisqlmalloc.so 文件还能被测试多久,剩余的测试时间。默认的测试时间是 7*24 小时。

注册成功后,不会显示以上信息。可以通过调用 haisql_malloc_version() 函数,显示版权信息。

第四章 配置文件和配置选项

外部配置文件:

默认的配置文件 1 是: /etc/libhaisqlmalloc.conf

默认的配置文件 2 是: 当前 libhaisqlmalloc.so 文件所在的目录下的 libhaisqlmalloc.conf 文件

在 LibLibhaisqlmalloc 库的使用过程中,将会依次使用配置文件 1 和配置文件 2, 获取配置信息。如果配置文件 1 存在,将不读取配置文件 2。如果两个配置文件均不存在,那么就按照默认值执行。

每个配置参数即可以通过 conf 文件来修改,也可以通过调用函数 mallopt( int int_key, int int_value ) 来修改定义。其中 int_value 是设置的值。

LibLibhaisqlmalloc 库的 key 值范围是 10000 —-10016。

配置参数如下:

uint_microseconds_thread_background_sleep

类型:无符号整数

默认值:500

意思:默认值是 500, 表示后台线程至少每 500 微秒会主动唤醒一次,执行后台任务。实际后台线程在工作中,每次检测到提前准备 2M 大块的需求,将唤醒时间周期降低一半直到 50 微秒,否则恢复使用默认值。

范围:大于等于 50, 低于 50 的配置将自动修改为 50

Mallopt int_key=10000

uint_wait_microseconds_between_release_2m

类型:无符号整数

默认值:4000

意思:默认值是 4000, 表示后台线程最大每 4000 微秒可以释放 1 个 2M 块,每秒最大允许释放 512M 内存。这个配置参数降低了内存的释放速度,因此,提高了重复使用待释放内存的概率,提高了性能。

Mallopt int_key=10001

bool_enable_thread_background_clear_cpu_cache

类型:Boolean

默认值:true

意思:后台线程将会自动清理释放给操作系统的内存的 CPU cache, 将这部分内存从 CPU 的 L1L2L3 级 Cache 中彻底清除掉,此功能将提升内存分配库和应用程序的性能,一般会提升内存分配库 10% 以上的性能,启用后将会增加后台线程的工作负荷。

Mallopt int_key=10002

bool_enable_hugepage2m

类型:Boolean

默认值:true

意思:是否启用 hugepage2m, 这里是 hugepage2m 的总开关。满足 2 个条件,将尝试启用 hugepage2m,第 1 个是启用了后台工作线程,第 2 个条件是本参数设置为允许,后台线程才会自动尝试启用 hugepage2m。这种设计方案主要是考虑 Hugepage2M 的内存申请都会实际分配物理内存,开销比较大,启用后台线程才能避免这种开销。

Mallopt int_key=10003

ulong_size_use_thread_background_threshold

类型:无符号 64bit 长整数

默认值:16777216

意思:申请内存总量超过配置值(默认是 16M) 将会尝试启用后台线程

注意:此配置项不支持 K/M/G/T 的后缀,必须是一个纯数字.

Mallopt int_key=10004

bool_enable_thread_background

类型:Boolean

默认值:true

意思:是否启用 后台线程, 这里是后台线程总开关。有两种情况下,将尝试启用后台线程,一种是申请内存总量超过配置值 (见 ulong_size_use_thread_background_threshold 的说明),另外一种是检测到创建了额外工作线程, 也会自动尝试启用后台线程。

Mallopt int_key=10005

bool_enable_auto_bind_cpu

类型:Boolean

默认值:true

意思:是否启用"工作线程自动绑定 CPU".此功能启用后,将会在线程第一次申请内存时自动检查是否已经指定了运行的 CPU 范围,如果没有指定,将自动执行线程自动绑定 CPU:

首先检测 CPU 是否支持超线程,如果是支持超线程的 CPU, 将自动将工作线程绑定到一个物理 CPU 上的多个逻辑 CPU 上(这种在同一个物理 CPU 上的在逻辑 CPU 上的线程切换不会造成 Cache 颠簸,具备更高的线程切换性能)。

如果 CPU 不支持超线程,将自动将工作线程绑定到同一 NUMA node 下的 1—2 个相邻的物理 CPU 上。(见后面配置参数 uint_auto_bind_cpu_count)

Mallopt int_key=10006

bool_enable_numa_memory_bind

类型:Boolean

默认值:false

意思:是否启用 NUMA node 内存绑定,目前只有中高端服务器才支持 NUMA, 传统 PC 和低端服务器(单一 CPU 槽位的服务器)不支持 NUMA。目前默认是关闭的,打开此选项,将会在支持 NUMA 的服务器上启用按工作线程的所在 NUMA node 优先的内存分配策略,带来性能提升。

Mallopt int_key=10007

uint_vt_thread_local_wait_reuse_limit_size

类型:无符号整数

默认值:8

意思: 限制用于保存备用 thread_local 的池子大小的限制大小为 8.即当线程退出时,如果备用 thread_local 池的空间超过 8, 并且该 thread_local 相关的所有 malloc 指针都已经释放,那么这个 thread_local 数据结构将执行析构。否则,这些数据那么这个数据结构将被缓存下来用于重用,在下次新的线程创建后,将优先复用这些备用的 thread_local 数据,

这种 pool 池减少了大量线程反复创建退出,一堆 thread_local 内部相关对象反复创建和析构带来巨大的执行开销浪费。

Mallopt int_key=10008

bool_enable_page_prefault

类型:Boolean

默认值:false

意思:申请内存时是否实际分配 4KiB page 页面的物理地址,这个功能点启用后,将会非常浪费空间,将会略微提升用户程序的性能。因为集中产生实际物理页面的分配,会提升一些程序的稳定性(例如:避免 Facebook fstring 早期版本末尾‘\0’延迟实现 Bug)。

注意:在任何情况下,申请 hugepage2M 的页面都会实际分配物理地址,Hugepage2M 没有修改配置的地方。

Mallopt int_key=10009

bool_enable_thread_background_use_page_prefault

类型:Boolean

默认值:false

意思:后台线程申请 4KiB page 页面的 2MiB 大块时是否实际分配物理地址,只有前面这个配置项 bool_enable_page_prefault 为 false, 那么这个功能点才有可能配置为启动。

这个功能点的主要区别是这个是配置为只有后台线程才能提前分配 page 页面的物理内存,其他线程直接从操作系统中申请的内存,还是传统的方式,不会提前分配物理内存。该功能启用后,将会略微提升用户程序的性能(后台线程每申请 1 个 2MiB 会产生 512 次缺页中断,后台会很忙,后台总共只有一个线程)。因为集中产生实际物理页面的分配,会浪费内存,会提升一些程序的稳定性(例如:避免 Facebook fstring 早期版本末尾‘\0’延迟实现 Bug)。

Mallopt int_key=10010

如何在代码中提前启用 hugepage 和后台线程,提升性能表现?

Mallopt int_key=10011, 是一个功能提前启用点,表示提前启用 hugepage,而不考虑其他配置参数

1)如何在代码中提前启用 hugepage,提升性能表现?

方法:mallopt(10011, 1);

Int_key = 编码 10011 内部定义为立即尝试启用 hugepage。

注意:将强制启用 hugepage2m,而不考虑其他配置参数。

注意:必须要保证机器内部配置有 Hugepage2M 的页面,才会真正启动 hugepage2M。具体的 hugepage2M 的配置方法见《第五章产品介绍和说明 第 11 节关于 HugePage 的一些建议》。

Mallopt int_key=10012, 是一个功能提前启用点,表示提前启用后台线程, 而不考虑其他配置参数

2)如何在代码中提前启用后台线程,提升性能表现?

方法:mallopt(10012, 1);

Int_key = 编码 10012 内部定义为立即尝试启用后台线程。

注意:将强制启用后台线程,而不考虑其他配置参数。

uint_thread_background_div_number_calc_prepare_2m

类型:无符号整数

默认值:8

意思:后台线程计算需要提前准备的 2M 大块时的除数,默认值是 8, 就是说内存使用 2M 块总量 / 除数 8 = 需要准备的 2M 大块的数量。

范围:大于等于 2, 低于 2 的配置将自动修改为 2

Mallopt int_key=10013

uint_thread_background_need_prepare_2m_count_limt

类型:无符号整数

默认值:8

意思:后台线程需要提前准备的 2M 大块的数量限制。默认值是 8, 表示限制需要准备的 2M 大块的数量为不超过 8, 即限制最多提前准备 8 个 2M 大块。(计算方法见上面一条)

Mallopt int_key=10014

uint_microseconds_thread_background_force_release_one_2m

类型:无符号整数

默认值:500000

意思:默认值 500000, 表示后台线程每 500000 微秒(500 毫秒)强制释放掉一个 2M 大块。这个功能可以在空闲时自动释放掉提前准备的多余 2M 大块内存,实现更低的内存占用。

如果这个参数配置为零,表示彻底关闭掉这个功能。

Mallopt int_key=10015

uint_auto_bind_phy_cpu_count

类型:无符号整数

默认值:2

意思:默认值 2, 表示启用"工作线程自动绑定 CPU"后(见前面 bool_enable_auto_bind_cpu

的配置说明),如果 CPU 不支持超线程,将自动将工作线程绑定到同一 NUMA node 下的 1—2 个相邻的物理 CPU 上。如果 CPU 支持超线程, 此参数无效。

范围:1 或者 2, 超出范围的配置将自动修改为 2。

Mallopt int_key=10016

第五章 产品介绍和说明

  1. 产品介绍

(1)libhaisqlmalloc.so

这个是库文件,适合在 Linux 操作系统下的内存分配库。

(2)test_malloc_use_so:

这个是 linux 下的已经编译好的一个测试文件。

性能测试软件的用法:

1)测试用法 1: ./test_malloc_use_so

2)测试用法 2: ./test_malloc_use_so —help

Usage: test_malloc_use_so test_count loop_count thread_count

Default: test_count 1000000

Default: loop_count 2

Default: thread_count 1

3)测试用法 3: ./test_malloc_use_so 10000 2 8

(3)test_malloc_use_so.cpp 文件:

这个文件是上面所述的性能测试文件的 C++ 源代码,用于进行性能测试比对。这个测试软件是完全自研的开源代码,为了方便大家使用和修改,Apache2 开源协议,提供了全部源码。

为了方便软件开发者和系统管理员,更好的使用内存分配库,需要对 Libhaisqlmalloc 内存分配库的内部实现有更多细致的理解,因此在后面详细介绍了内存库的各种参数的设计和实现方案。

  1. 性能高的主要原因

Libhaisqlmalloc 高性能内存分配库,用于降低频繁分配、释放内存造成的性能损耗,并且有效地控制内存碎片。Libhaisqlmalloc 采用了新的软件技术,具备极高的性能表现。目前 malloc 4K 比 Google tcmalloc 和 Linux 默认内存分配库 ptmalloc 快了 2 个数量级。在 Intel CPU 9400F(2.9G 默认频率) 下测试 100 万次 malloc4KiB 平均一次 4KiB malloc 操作, ptmallo 和 tcmalloc 需要近一千 ns, 而 Libhaisqlmalloc 只要 7ns。

Libhaisqlmalloc 对多线程做了很多优化,所有分配基本上不存在锁竞争。分配给线程的内存全部是按需分配,提高了内存利用率,不会浪费内存,也减少了内存碎片。

Libhaisqlmalloc 为每个线程分配了一个线程局部的 Thread Local Cache,线程申请的 2000KiB 以下内存都是在其 Thread Local Cache 中分配的,由于是 Thread Local 的,所以是无锁的。同时,Libhaisqlmalloc 维护了进程级别的 Cache,所有的 2M 大小的块都在这个进程级别的 Cache 中分配,这个 Cache 使用了一种多生产者多消费者 MPMC 的无锁队列,开销小,性能高。

Libhaisqlmalloc 内存分配库性能高的主要原因是:

1)后台线程是关键。消耗时间大的系统调用,直接用后台线程处理,提高前端的性能,后台线程处理比较耗时的那些操作全部处理了。例如,一次 mmap /ummap 2M 系统调用,大约开销会有几百微秒,如果用后台线程去处理,提前准备好 2M 块,以及延迟释放 2M,大约只需要几十个 ns 就可以获得 / 释放 2M 块,这块提高了 “获得 / 释放 2M 块” 这个功能点大约 1 万倍性能,分摊到每次 8byte 申请上的时间前端表现就是申请 8byte 与申请 4K byte 消耗的时间几乎一致,都是几个 ns 的时间。

2)Bitmap 算法。现代 CPU 受内存墙影响性能,Bitmap 算法占用内存最小,占用释放一个基本空间只有 1 个 bit 修改, 而传统算法是放一堆指针,修改一个指针就是 64bit 的修改量,实际要修改很多地方。如果申请和释放大量内存,从数据修改总量上看,Bitmap 具备更低的数据修改总量。

  1. 连续性好的数据结构。传统算法中的链表数据结构由于 cache 不友好性能很低 。LibLibhaisqlmalloc 库采用连续性好的数据结构(例如 vector)保存数据,性能很高。

  2. 无锁或者锁冲突比较少的数据结构,提升多线程性能。

5)Libhaisqlmalloc 库重写了部分常用的 std 库,提供了更好的性能。

  1. 内存块种类 group 说明

Libhaisqlmalloc 库的管理内存范围是:

1 字节到 2000KiB 字节使用 Bitmap 方式分配。

2000KiB—-2MiB(含 2MiB)之间的内存是由操作系统 mmap 方式直接分配或者由后台线程间接从操作系统 mmap 方式申请得到。

2MiB 以上的内存实际是由操作系统 mmap 方式直接分配。

1)1-3408 字节以下是小内存,使用 17 种不同大小的块,17 种 group 进行分配。

8 字节以下单独使用一组 group, 按照 8Byte 对齐,其余是 16Byte 对齐,使用其他 16 组 group。

17 种 group 的总体块大小依次是 4K, 8K, 8K×2, 8K×3, 8K×4, 8K×5, 8K×6, 8K×7, 8K×8, 8K×9, 8K×10, 8K×11, 8K×12, 8K×13, 8K×16, 8K×32, 8K×64。

每个 group 块的内部都有一个 512bit(64Byte) 的 Bitmap 数据结构用于申请和释放内存,允许多 bit 申请,所以另外还有一组 256 字节压缩 bit 数据结构用于保存每个已经申请内存的指针对应的内存大小等 cookie 参数。每个 group 都是 Cache Line 64Byte 对齐的,group 数据结构自身总体大小约是 384 字节,最大对应 512 次内存申请,平均每次内存申请的 cookie 数据结构平均管理内存占用开销大约只有 6bit,平均每指针的管理内存开销小于一个字节。这种高度压缩的 bit 数据结构比传统的内存分配库的 cookie 数据结构小 N 倍,性能极高。这种内存的所有处理流程均是通过 Thread Local 方式实现无锁。

2)3409 字节 —-10KiB 是小内存(N1KiB) 或者中内存 (N4KiB),使用 2 种不同大小的块,每个 group 子块是 1024 或者 4096,根据情况分别选择不同大小的 group 进行处理。这种内存的所有处理流程也是通过 Thread Local 方式实现无锁。

3)10KiB-2MiB 字节是中内存,使用 4KiB 对齐的分配策略,由总大小是 2MiB 的 group 进行分配。其中 (10KiB+1)—-2000KiB 字节的申请会在 4KiB 对齐的 group 内分配;(2000KiB+1)-2048KiB 之间实际分配 2048KiB(最大浪费 2.4%),是在 group 外分配,这里会优先使用进程级别的 Cache—— 多线程 MPMC 无锁队列中的 2MiB 块,更加直接,所以,性能表现会比 group 内的 2000KiB 的分配性能更高。group 的数据结构是 Cache Line 对齐的,group 自身的总体大小约是 128 字节,额外使用了 4KiB 的管理空间,最大对应 512 次内存申请,每指针的管理内存占用开销大约只有 66bit, 这种高度压缩的 bit 数据结构比传统的内存分配库的 cookie 数据结构小 N 倍,性能极高。这种内存的所有处理流程也是通过 Thread Local 方式实现无锁。

4)2M 以上内存,是大内存,由于默认自动支持 Hugepage2M, 2MiB 以上均是按照 2MiB 对齐方式分配的, 使用 2MiB 对齐的策略,使用系统调用 mmap 进行分配。在存在 Hugepage2M 的情况下,优先使用 Hugepage2M。大内存的 cookie 数据结构是一种优化的 unordered_map 数据结构(开放地址法 + 线性探测),性能远远好于 std::unordered_map 的实现,用于指针 ptr_void 与申请内存大小 size 等 cookie 数据的关联关系查询,大内存在实际应用中情况比较少,处理并发的代码非常简单,直接使用了标准锁 std::mutex 锁,来处理并发。

  1. 内存地址对齐和默认支持 SSE 指令集

Libhaisqlmalloc 库有良好的分配内存地址对齐,内存地址对齐的表现优于多数内存分配库的技术方案,默认支持 SSE(Single Instruction Multiple Data)单指令多数据技术指令集,方便开发高性能计算密集的高性能用户代码。对于 N*16 字节 ( N = 任意数), 默认分配的内存地址都是 16 字节对齐的, 默认支持 SSE 指令集。(C++ 委员会建议尽量 16 字节对齐,以便默认支持 SSE 指令集)

支持 C17 带 std::align_val_t 参数的 new 等, 具体见下一章关于对外函数接口的说明。 因此, 对于 C17 以后可以通过支持 std::align_val_t,进而支持更多 SIMD 指令集 。

对于 N*4KiB 字节, 分配的内存地址都是 4KiB 字节对齐的。

对于 N*2MiB 字节, 分配的内存地址都是 2MiB 字节对齐的。目前的版本最高支持到 2MiB 对齐。

  1. 内部碎片

Libhaisqlmalloc 库碎片表现非常优秀:Libhaisqlmalloc 库内部碎片平均浪费小于 10%,远好于很多库的设计。Libhaisqlmalloc 库内部碎片设计要求是 32Byte—-10KiB 的内存分配,内部碎片平均浪费小于 10%,最大碎片浪费率小于 20%。在满足这个内部碎片率的前提下才会考虑性能,从多种 group 方案中选取折中的技术方案。因此,低碎片率是基本要求,其次才是性能。

申请不同大小的内存,因为内部对齐而产生碎片,这种碎片称为内部碎片。

内部碎片1:

1-8 字节首先按照 8 字节对齐,内部碎片1最大是 7 字节;

9 字节 - 4KiB 按照 16 字节对齐,内部碎片1最大是 15 字节;

(4KiB+1) 字节 —-10KiByte 按照 1KiB 对齐,内部碎片1最大是 1023 字节;

(10KiB+1) 字节 —-2000KiByte 按照 4KiB 对齐,内部碎片1最大是 4095 字节;

C++ 委员会建议内存分配 16 字节以上的场景下,尽量采用 16 字节对齐,以便默认支持 SSE 指令要求的对齐方式, 提高软件的性能表现。Libhaisqlmalloc 库对于 9 字节以上的内存申请均是 16 字节对齐。

内部碎片2:

对于 16 字节 —-3408 字节的内存,Libhaisqlmalloc 库有第 2 次对齐的过程,因此,同时存在内部碎片1和内部碎片2。由于 group 内部块对齐而产生碎片,这种碎片称为内部碎片 2。

内部碎片 2 是可以调整的碎片,换不同大小的 group 块,获得不同的性能表现和不同大小的内部碎片 2。group 类型过多会带来更多的外部碎片,因为每种 group 都会浪费更多的内存, 所以,我们目前的设计方案中 group 类型只有区区 18 种,就覆盖了 1-2M 字节的所有分配方案.

Libhaisqlmalloc 库的 Bitmap 算法搜索连续 bit 数越多性能越低,低 bit 数的内存分配方案会有更好的性能,因此总体的碎片设计规则是:在满足最大内部碎片率低于 20% 的前提下,尽量选择 bit 数低的 group 方案 (在 18 个 group 中选择)。malloc 的调用从多种 group 方案中选取一个折中的技术方案,适当的内部碎片,尽可能高的性能.

具体的设计方案:

16 字节 —-3408 字节 参见后面附录一内部碎片的设计方案。内部碎片 2 最大碎片率是 16.67%,平均碎片率小于 8%。

3409 字节 —-4KiB 按照 4KiB 分配,最大碎片 687Byte,最大碎片率 16.77%,平均碎片率 8.4%。

(4KiB+1)—-2000KiB 的 malloc 的内部碎片浪费很小,只有一次对齐.所以,只有内部碎片1。

4KiB—-10KiB 是按照 1KiB 对齐分配,最大浪费 1023 字节.。最大碎片率 20%,平均碎片率低于 10%。

10KiB—-2000KiB 是按照 4KiB 对齐分配,最大浪费 4095 字节。平均碎片率远远低于 10%。

内部碎片3:

对于 2000KiB 以上的内存,Libhaisqlmalloc 库有1次额外的对齐过程,这种对齐是按照 2MiB 字节对齐,因此,存在内部碎片3。2MiB 以上内存,是大内存,由于支持 Hugepage2M, 为了提供一致的效果表现,2000KiB 以上均是按照 2MiB 对齐方式分配的, 使用了 2MiB 对齐的策略,最大浪费 2097151(2MiB-1) 字节。

这种场景下,如果申请内存量不是 2MiB 的倍数,那么就会有内部碎片3带来的浪费(好在这种情况在应用中很少出现)。如果申请内存量是 2MiB 的倍数,那么就不会有任何碎片浪费。

测试代码的开头有测试内存分配库内部碎片的情况, 但是实际情况要复杂的多,Linux ptmalloc 库虽然内部碎片很低,但是有巨大的外部碎片,总体的碎片浪费非常严重。

  1. 外部碎片:

Libhaisqlmalloc 库有很低的外部碎片。LibLibhaisqlmalloc 库设计思路是优先考虑低外部碎片和低内存浪费的方式,宁可在一些场景下以大幅度牺牲性能表现为代价,来换取低外部碎片和低内存浪费。

外部碎片的定义:由于执行过程中反复的 malloc/free 不同大小的内存块,会造成连续的内存中间一些块被占用,缺乏连续空间用于大块和连续块的分配,这种碎片是外部碎片,会造成的空间浪费,会占用更多的空间。

在减少外部碎片方面,Libhaisqlmalloc 库有更好的解决方案,一个设计思路就是线程内部完全不做任何空间缓冲的方案: 内部的内存申请原则上采用按需分配,在需要时会立即申请,释放时都是立即释放, 不做任何缓存。这种方式降低了性能,提升了内存的利用率,有更小的内存消耗.更少的外部碎片。

一)小内存无缓存,不会预分配备用。每一次 free 释放都是真实的释放,不会缓冲起来下次再用,这样保证只要一大块内存内的多个连续块释放后,就可以获得彻底连续的空间,不会有传统算法缓存带来额外碎片的问题。例如:有些传统内存分配库为了性能将一些指针缓冲起来下次再用,而有可能刚好就缺这部分块就可以空间连续。

二)所有大小的 group 都是动态分配,不存在预分配,free 时只要 group 全空就一定会释放,不会保存起来等待下次再用,这样就不存在预留内存块的浪费,也不会因为预分配的 group 块不符合实际需求造成浪费,简单高效的就实现了低浪费,低外部碎片。

三)低碎片率的内存分配的策略,首先是采用了 18 种 group(远远低于很多库的设计数量), 这样不同大小的内存分配走不同的 group, 尤其是常用的 256Byte 以下,都是单 bit 分配,完全没有碎片浪费,高于 256Byte 采用了 2 倍增长的 group 类型,有足够的区分度,是小 bit 数分配,碎片浪费也比较少。

四)搜索算法上使用了一种低内存浪费的搜索解决方案,首次适应算法(First Fit)+循环首次适应算法(Next Fit)同时使用。当 free() 调用后,下次 malloc 将使用 First Fit,从头开始搜索,这样保证释放掉的内存可以立即被重复使用.扩大了搜索范围,大幅度降低了性能,提高了碎片利用率,第二次 malloc 同样大小的数据块,将使用 Next Fit 算法,从上次分配内存的地方继续向后搜索和分配,加快内存分配速度.这样既保证了性能,又保证了刚释放的空间立即复用不浪费,减少了碎片,大大节约了内存。

五)一种低碎片率的古老的原始的 Bitmap 算法,这种算法不同于传统算法(产生大量碎片的)buddy 伙伴分配算法,算法的优点是自身不会产生额外碎片,具备非常低的碎片。缺点是需要检索更多的数据,执行更多的指令,理论上会大幅度降低性能表现。

原始的 Bitmap 架构算法,就是一次在一个 Cache Line 中(64Byte,512bit 内存中)对多个连续 N 个全零 bit 搜索,找到了连续 N 个零 bit 就可以在这里映射 bit 对应的地方分配内存了,并且 free 时会自动实现大小块自动合并,连续块自动合并,完全没有其他 (非 Bitmap) 模型的连续块合并的开销和大小块合并的开销,因此,一方面碎片极小,一方面合并数据块的性能极高.比其他 (非 Bitmap) 算法有更小的开销.每个 group 子块空间 512Bit(即 64Byte)刚好是 Intel CPU 的一个 Cache Line 的长度, 保证了连续申请和释放内存都在同一个 Cache Line 中操作,性能极高,同时这个长度也足够小,也可以尽快及时将全空的 group 子块彻底释放。这个长度设计在性能和内存浪费之间做了权衡。单个零 bit 的搜索性能最高,多个连续零 bit 的搜索开销要大很多,bit 数越高开销也越大,因为要比较更多次数,以及更多的管理空间占用,更多的 bit 数修改和更新,设计最高支持连续 500 个零 bit 的搜索。因此,设计时如果有多个方案可选要尽量实现低 bit 数的设计,才会实现更高的性能。

使用原始的 bitmap 算法 + Cacheline 对齐 + 现代的 Intrinsic 函数可以在现代 CPU 上实现极高的性能。Bitmap 算法主要是 C++ 实现的,不需要 bit 对齐(不同于 buddy 伙伴分配算法),可以从任意 bit 点开始分配连续 N bit 内存,因此算法自身不会产生额外的碎片,内部使用了几个现代 CPU 的 Intrinsic 函数(等效于汇编语言,兼容多种 CPU,最多只用到 SSE4.2 版本)来加速性能。由于现代 CPU 的多发射特性,相邻的多条指令处理同一个 CacheLine 的数据是高度并发的,可以在一个时钟周期内执行多个相邻指令(现代 CPU 最高是 10 并发即同一时钟周期并发 10 条指令),CPU 内部的工作情况比几十年前的情况有巨大的变化,同一个 Cacheline 中连续多个数据的处理几乎不消耗 CPU 时间,大量 CPU 时间其实是消耗在等待内存到 CPU L1 的存取时间开销上,现代 CPU 受内存墙影响性能,Bitmap 算法具备超低的管理内存占用(尤其是 1bit 的算法),因此在现代 CPU 上使用 CacheLine 对齐的 bitmap 算法,可以实现极高的性能。

总之 Libhaisqlmalloc 库设计时用古老的 Bitmap 原始算法,实现了更低的碎片率,但是需要执行更多的指令,理论上会大幅度降低性能表现,设计上采用了理论上会大幅度降低性能表现的技术方案来换取更低的碎片率和减少内存浪费。现代 C++ 新技术采用多发射友好的数据结构和新的算法设计(例如使用 Intrinsic 函数),在一个时钟周期内 CPU 可以并发执行更多的指令,实现极高的性能,最终软件的性能表现反而比传统算法更快。

六)按 bit 数分配的外部碎片浪费极小。

低 bit 数的总体设计方案,保证了外部碎片很小。具体如下:

256 字节以下大小的内存分配全部是 Bitmap 1bit 的分配方式,这种方式保证了常用的小字节下,不存在任何外部碎片,每一个 bit 空间都可以得到分配,并且性能更高。

257 字节 - 3408 字节其中一部分采用多 Bit 的分配方式,这种分配方式会产生少量外部碎片,多 Bit 的最大 bit 数(参见附录一)只有 9bit, 因此最大浪费碎片 bit 数是 8bit, 相对总量 512bit,这种浪费是非常少的,单个非连续空间最大浪费只有 1.56%。

3049 字节 - 4KiB 大小的内存分配全部是 Bitmap 1bit 的 4KiB 分配方式,这种方式不存在任何外部碎片,每一个 bit 空间都可以得到分配,并且性能极高。

4KiB-10KiB 默认按照 1K 对齐(如果不是 8KiB 的情况),这种分配方式会产生少量外部碎片,最大 bit 数是 10bit, 因此最大浪费碎片 bit 数是 9bit, 相对总量 512bit,这种浪费也是非常少的,单个非连续空间最大浪费只有 1.76%。

(10KiB+1)-2000KiB 按照 4KiB 对齐,采用多 Bit 的分配方式,会产生外部碎片,并且这里是设计方案中最容易出现外部碎片的地方,主要集中于大 Bit 数的分配数,因为检索空间的范围是从 1bit 到 500bit 个连续零,这个范围是非常大的一个范围,但是由于 malloc 检索空闲空间是任意连续 N bit 的顺序全搜索模式,没有 bit 对齐的假设,并且常用 bit 数均是小 bit 数,超过 32KiB(即 8bit) 的场景非常少见,因此,实际使用中一般也很少出现明显的外部碎片问题。

七)工作线程内部不会缓存 2MiB 大块内存,也不会预先分配给线程 2MiB 大块内存备用。

空闲的 2MiB 大块内存是所有线程共同管理的,因此,不存在任何线程内部浪费。每次 4K 连续块 group 对应的 2MiB 空闲块都会立即释放到一个多生产者多消费者 MPMC 无锁队列中,不会在线程内部的数据结构中保存等待下次再用。不存在传统内存分配库线程内部的 2MiB 块浪费,也没有预分配给线程 2M 块数量不符合线程的实际需求造成浪费,完全不存在传统内存分配库中一个线程已经空闲的大块内存无法被其他线程利用的情况,极大的节约了内存,减少了碎片。

对于短期内不会复用的多生产者多消费者 MPMC 无锁队列中的空闲 2MiB 块,由后台线程智能的释放给操作系统。

八)Libhaisqlmalloc 库的低缓存的设计,以大幅度牺牲性能表现为代价来换取低外部碎片和低内存浪费。Libhaisqlmalloc 库在各级处理流程中都是尽快释放,不做额外缓存。每级操作流程中如果有大量缓存的话,那么就可以走短流程,实现更高性能,如果没有缓存,就必须要走长流程,向上级申请或者释放内存,严重降低了性能,甚至有个别时候会有多级长流程操作,导致性能大幅度下滑。

7.2MiB 大块内存的统一进程缓存:

Libhaisqlmalloc 库使用了现代 C++ 无锁编程技术解决空闲 2MiB 大块数据管理问题。

工作线程的 2MiB 块空闲后会立即释放到按 NUMA CPU node 分组的容量 4096 的多生产者多消费者 MPMC 无锁队列中,这种释放开销只有几十 ns(二次 atomic 存取开销)。

这里是标准的多生产者多消费者场景,所有的线程既是生产者又是消费者,所以,这里采用了一种多生产者多消费者的无锁 MPMC 数据队列,释放掉的 2MiB 块数据对所有线程完全共享,提前给队列预分配了 4096 个数据存储结构,没有 ABA 问题( 队列大小固定不会申请释放内存),是一种连续的数据结构,入队 / 出队操作只是二次 atomic 存取开销,性能很高,不会有传统锁冲突时的休眠时延。

释放掉的 2MiB 块存储在无锁队列中可以立即被同一 NUMA CPU node 的多个工作线程复用,重用开销也只有几十 ns(二次 atomic 存取开销)。

多生产者多消费者 MPMC 无锁队列的容量是 4096 个指针空间,存储的 2MiB 大块内存数据对所有线程透明共享, 一般情况下此容量已经足够满足需求。如果此无锁队列空间不足,那么将启用一个备用的锁保护的传统队列,传统队列有锁开销,但是这种情况几乎不会出现。

8.Thread Local 算法:

目前 C++ std / boost 库以及 Linux pthread 库均提供了 Thread Local 数据结构,这些库接口的 Thread Local 算法上存在优化空间,并且存在库依赖,因此重新实现了一个高速的没有库依赖条件的 Thread Local 专用数据结构,有更高的性能(提升数倍),以及更加良好的适应性 (没有外部依赖),避免了一些场景下的多个 so 库初始化顺序的兼容性问题,提高了内存库的兼容性。

主要流程和开销情况:

以 malloc 4KiB 的动作为例, 平均开销是 7ns (Intel CPU 9400F 2.9G 主频下 100 万次测试), 主要流程和开销如下:

1)so 动态链接接口的开销。动态链接有比较大的开销。这部分是固有开销,无法被优化掉。

  1. 申请大小为 2000KiB 以上,走操作系统 mmap 调用, 保存长度等 cookie 数据到一个传统锁保护的全局 hash_map 的数据结构中,返回 mmap 指针。

  2. 检测是否是 so 文件初始化尚未完成前就开始执行 malloc 申请的内存(即紧急模块的内存申请),这部分有一个专门的小模块单独处理,使用 sbrk 申请一块内存,然后切成小片分配给用户,每次多申请 16 字节,用于保存 cookie 到返回空间前 16 字节, 返回得到的 sbrk 指针。

  3. 检测是否需要启动后台线程,如果需要,则启动后台线程。后台线程检测是否需要启用 hugepage2M。如果需要,则检测操作系统是否支持 hugepage2M,设置允许使用 hugepage2M 标志。

5)thread local ptr.get() 的查询开销。

  1. 检测跨线程释放无锁队列中是否有足够多的待释放指针,如果满足条件(>=8),则执行集中释放,减少 cache 颠簸,提高平均性能(考虑不能有过大的延迟和抖动,取了一个比较小的值)。

  2. 按照数据块大小选择不同大小 group 的开销,有几个判断:

首先是按照申请内存的大小进行 8 字节 / 16 字节对齐

1—-8 字节走 8 字节;

9 字节 —-3408 字节 16 字节对齐走一个跳表查询,会在 17种不同大小的 group 中选择合适的 group,可选的有 N 个方案,优先少 bit 数,同时限制最大浪费不能超过20%,平均浪费小于 10%. (参见附录中的详细说明);

3409 字节 —-4KiB 的会申请 4K;

(4KiB+1)—-10KiB 的会在 1KiB/4KiB 的 group 中选择最合适的 group 对齐,8KiB 选择 24KiB, 其他选择 N1KiB,例如 4097 字节选择5个 1KiB 的连续块,这样的浪费比较少;

10KiB—-2000KiB 的全部是 4KiB 的 N 倍数, 2000KiB 就是连续 500 个 4K 块。

使用了预处理软件定义好最优方案,自动生成类似于跳表数据结构代码,实际代码就是几个简单的判断+类似跳表的 C++ 对象, 性能极高。

  1. 各种大小的内存的内部分配流程就是按照待申请的 bit 数,执行 Bitmap 申请内存流程。Bitmap 代码开销只占总体开销的一小部分。

  2. 同步原语的开销。在一些数据结构的创建过程中有大量的同步原语的使用,例如用于与后台线程的数据交换等,另外 Bitmap 对于所有线程 thread_id 相关 cookie 数据均进行了写数据同步(通过使用 sfence 内存屏障的方式),保证指针的跨线程使用和释放是线程安全的。

以 free 4KiB 动作为例,平均开销是 7ns (Intel CPU 9400F 2.9G 主频下 100 万次测试测试), 主要流程和开销如下:

1)so 动态链接接口的开销。动态链接有比较大的开销。这部分是固有开销,无法被优化掉。

  1. 写同步原语的开销,在 free 内部的第一个步骤就是执行写同步(使用 sfence 内存屏障),保证释放前与这个指针相关的写操作都已经完成。这部分是固有开销,是提升多线程稳定性的代码,开销很大。

  2. 检测是否是 so 文件初始化尚未完成前就开始执行 malloc 申请的内存(紧急模块的)内存申请,从多申请的 16 字节中获得长度等 cookie 数据,执行紧急模块的释放流程。

  3. 检测是否是空指针(空指针立即返回)。

  4. 检测是否是已经处于进程退出的析构 so 内部全局数据结构的最后退出阶段,此阶段不作处理。

  5. 检测是否是直接从操作系统 mmap 方式申请的 2MiB 以上大内存块,从一个传统锁保护的全局 hash_map 的数据结构中获取长度等 cookie 数据,释放这种内存块直接走 munmap 系统调用, 从一个全局 hash_map 数据结构中获得长度等 cookie 数据的数据结构。

  6. 检测是否是非法指针,非法指针立即返回,不做处理。非法指针校验使用了 64bit 数据校验,后面还有一次指针对应的 bit 数的非零检测校验,两次独立的校验过程可以处理全部非法指针的情况,减少非法指针对程序正常运行的干扰,提高库程序的健壮性。

  7. 检测是否是跨线程指针释放 (检测指针对应的 group 类型的数据结构中的 thread_id 标志与当前线程的 thread_id 标志是否一致),对于 A 线程申请, B 线程释放的指针,需要执行跨线程释放流程,通过指针数据 cookie 找到申请内存的线程 A 内部结构,直接保存指针到申请线程 A 内部的一个无锁队列中,最终队列中的指针由当时申请内存的线程 A 择机释放,实现了高性能的跨线程释放。

  8. 检测指针数据 cookie 对应的 group 块大小,分别走不同块大小的 group 流程, 跳转到 18 种 group 中的一种类型, 错位 1—-(N-1)(N 是 group 类型单 bit 对应的长度)个字节的非对齐的指针会自动对齐,有更好的容错,支持少量错位容错,检索指针对应的待释放的 bit 数,将 group 内部的此 bit 数清零,实现避免重复释放。如果 bit 数为零,表示可能是重复释放的指针或非法指针,不做处理。

  9. 按照待释放的 bit 数,执行 Bitmap 释放内存流程。Bitmap 代码开销只占总体开销的一小部分。

  10. 同步原语的开销。在一些数据结构的析构过程中有大量的同步原语的使用。

从以上的 malloc/free 的内部主要流程和开销情况可以看出,malloc / free 的流程非常复杂,考虑的情况非常多,有大量的复杂流程,但是执行起来,平均开销这么小,所有流程的代码一定是经过高度优化的,并且一定采用了很多不同寻常的技术方案,解决了很多 CPU 的执行瓶颈问题,代码才能跑得如此之快。下面将简单介绍几个主要技术方案。

  1. 支持 HugePage:

LibLibhaisqlmalloc 库提供了对 Hugepage 的支持,可以大幅度提升应用程序的性能。Hugepage 可以大幅度减少 CPU TLB miss,提升大型应用程序的读写内存的速度。目前很多传统内存库不包含此功能点。

目前版本没有支持 Hugepage1G, 主要的原因是:Hugepage1G 浪费内存情况比较严重,存在一些场景下,由于每次申请或者释放的粒度过大,造成一定程度的内存浪费,并且每次申请和释放 1G 内存和缺页中断的时间开销远远高于 hugepage2M 的开销,最高会有大约 200 毫秒的后台线程开销,这样会给前端带来更多的延迟和更大的时延抖动。

默认启用 Hugepage2M,该配置可以关闭。只有在后台线程启用以后,并且操作系统提前配置了 Hugepage 页面,才会开始使用 Hugepage2M, 这种设计方案主要是考虑 Hugepage2M 的内存申请都会实际分配物理内存,开销比较大,启用后台线程才能避免这种时延抖动。相关配置参数:见 /etc/libhaisqlmalloc.conf 中 bool_enable_hugepage2m 的说明。

在很多场景下配置 Hugepgae2M 可以提升内存库的性能表现, 以及用户程序的性能表现。因此, 建议用户配置一部分内存为 Hugepage2M,根据不同的场景,可以采取不同的分配策略。建议在 Linux4.X 或者以上版本并且可用内存比较多的场景下,分配一半内存配置为 Hugepage2M。由于目前 Linux 操作系统下 Hugepage2M 内存不参与 swap,会导致虚拟内存更加紧张,在内存比较少比较紧张的场景下,减少分配 Hugepage2M 的总量,以便提供更好的 swap 性能。

提供 Hugepage2M 的支持方法:

方法一:修改 / etc/sysctl.conf 配置文件增加一行

vm.nr_hugepages=2048

这里 2048 表示使用 2048 个 Hugepage2M 页面,即分配 4G 内存为 Hugepage2M。

修改完配置文件后,需要重新启动生效,或者执行下列命令生效。

sudo sysctl -p

方法二:修改 / etc/default/grub 启动配置项,修改默认值

GRUB_CMDLINE_LINUX=””

GRUB_CMDLINE_LINUX=”transparent_hugepage=never default_hugepagesz=2M hugepagesz=2M hugepages=2048”

这里 2048 表示使用 2048 个 Hugepage2M 页面,即分配 4G 内存为 Hugepage2M。

修改完配置文件后,需要更新一次 grub, 然后下次启动才能生效。

sudo update-grub

下列命令可以显示 Hugepage2M 的使用情况

cat /proc/meminfo | grep Huge

AnonHugePages: 0 kB

HugePages_Total: 3200

HugePages_Free: 954

HugePages_Rsvd: 30

HugePages_Surp: 0

Hugepagesize: 2048 kB

  1. 支持物理内存提前分配:

LibLibhaisqlmalloc 库中的内存分配的物理地址分配,是可以配置为提前分配的。这样对于一些对性能有极致要求的场合可以提供更低的延迟,例如量化交易等,实现更高的性能表现。常规库的物理内存只有在内存使用中才能利用缺页中断分配物理内存,其实也是有不小的性能开销的。目前很多传统内存库不包含此功能点。

在任何情况下,每次申请 hugepage2M 的页面都会实际分配物理地址,Hugepage2M 没有修改配置的地方。一般情况下均是后台线程处理这种物理内存的分配,当后台线程忙不过来或者未被操作系统调度时,前端用户线程也会处理这种物理内存的分配。

有两处配置点可以实现提前分配常规 page 页面:

第 1 处配置:申请常规 page 页面, 可以配置为立即实际分配 4KiB page 页面的物理地址,默认配置为关闭此功能。参见配置 bool_enable_page_prefault 的说明。启用后,将会非常浪费空间,将会略微提升用户程序的性能。因为集中产生实际物理页面的分配,会提升一些程序的稳定性 (例如:避免 Facebook fstring 早期版本末尾‘\0’延迟实现 Bug),这个功能点启用后对所有大小的内存申请都有效 (包括管理内存等),对所有线程的内存申请都有效。

第 2 处配置:后台线程申请 4KiB page 页面的 2MiB 大块时是否实际分配物理地址,默认配置为关闭此功能。参见配置 bool_enable_thread_background_use_page_prefault 的说明。这个功能点的与前一个参数的主要区别是这个是配置只支持申请 4KiB page 页面的 2MiB 大块, 只支持后台线程,不会影响其他线程,不会影响 2MiB 大块以外其他大小的内存等情况(例如:N*2MiB 的情况,64KiB 的情况)。该功能启用后,将会略微提升用户程序的性能(后台线程每申请 1 个常规 page 页面的 2MiB 会产生 512 次缺页中断,后台会比较忙,后台总共只有一个线程,以便默认无锁操作)。因为集中产生实际物理页面的分配,会浪费内存,会提升一点性能,也会提升一些程序的稳定性。

  1. 支持释放内存自动清理 CPU Cache:

ibLibhaisqlmalloc 库提供了释放内存自动清理 CPU Cache 的支持。目前很多传统内存库不包含此功能点。

LibLibhaisqlmalloc 库可以将释放内存从前端工作 CPU 的 L1L2L3 级 Cache 中彻底淘汰,提供了更多空闲 CPU Cache 给内存库和应用程序使用,提升了内存库和应用程序的性能。

一般情况下,彻底释放给操作系统的内存还会占用宝贵的 CPU 缓存空间。LibLibhaisqlmalloc 库的后台线程在彻底释放 2MiB 大块内存给操作系统前,主动通知 CPU , 将这些缓存从 CPU 的一二三级缓存中彻底淘汰,这样就腾出了大量缓存空间给其它真正需要的内存,这项操作一般会提升内存分配库自身的性能 10%,并且会大幅提升大型应用程序的性能。

后台线程目前只对释放的单个 2MiB 大块内存使用此功能,对其他大小的内存释放不执行此操作。由于后台线程操作时不能影响其它线程和 CPU 的正常工作,因此,不能使用直接清空全部 cache 的做法(系统调用特权指令),只能使用一种从后台线程所在的 CPU 发起对前端多个工作 CPU 的 cache shootdown 的方法,逐个对每个 CPU 的 L1/L2/L3 cache 进行清理,通知全部 CPU 逐条清理 cache,执行追踪检索释放 CPU L1L2L3 cache 的过程会消耗非常多的后台线程 CPU 资源,增加后台线程的工作负荷。大约一个完全使用的 2MiB 大块对应的 cache 全部清理掉大约会消耗掉 550 微秒 (Intel CPU 9400F 2.9G 主频下) 的后台线程工作时间,是非常巨大的开销。

后台线程默认启用此功能,具体参见配置 bool_enable_thread_background_clear_cpu_cache 的说明。

  1. 支持线程与 CPU 自动绑定:

LibLibhaisqlmalloc 库提供了对线程与 CPU 的自动绑定的支持。目前很多传统内存库不包含此功能点。

现代 CPU 的数量很高,因此,高性能的工作场景一般要求绑定线程到少数几个(一般 1—-2 个)CPU 上,以便减少无效 CPU 切换,减少 CPU Cache 因为在多个 CPU 之间的切换而无效.但是,在编程的实践中很多程序并没有绑定 CPU, 因此,我们采用了下列措施,实现了一种工作线程与 CPU 的自动绑定算法,相关配置参数:见 /etc/libhaisqlmalloc.conf 中 bool_enable_auto_bind_cpu 的说明。

工作线程自动绑定 CPU.此功能启用后,将会在线程第一次申请内存时自动检查是否已经指定了运行的 CPU 范围,如果没有指定,将自动执行线程自动绑定 CPU:首先检测 CPU 是否支持超线程,如果是支持超线程的 CPU, 将自动将工作线程绑定到一个物理 CPU 上.如果不支持超线程,将自动将工作线程绑定到同一 NUMA node 下的2个相邻的物理 CPU 上。

LibLibhaisqlmalloc 库提供了对线程与 CPU 的自动绑定不会影响应用程序内部的对 CPU 的线程绑定功能,兼容这种情况。执行时检测如果应用程序已经执行了线程与 CPU 的绑定,则不会做任何修改.自动适应用户代码的实际情况,不会对用户的 CPU 绑定操作带来任何影响。如果用户代码在线程首次申请内存之后(此时 LibLibhaisqlmalloc 库已经默认执行了工作线程自动绑定 CPU)用户代码对线程重新绑定 CPU, 那么将会覆盖掉 LibLibhaisqlmalloc 库的绑定设置,也不会有任何影响。

建议用户应用代码不要跨 NUMA node 绑定多个 CPU, 即一个线程不要绑定到多个 NUMA node 上,否则,在一些场合下,会降低性能,但不会对 Libhaisqlmalloc 库内部的基于 NUMA 的内存绑定产生影响(因为 NUMA 的内存绑定都是动态获取 NUMA node 参数的,不会预设 thread 与 NUMA node 之间存在关联关系)。

  1. 支持 NUMA node 内存绑定:

LibLibhaisqlmalloc 库提供了对 NUMA 内存绑定 node 的支持。目前很多传统内存库不包含此功能点。

NUMA 是一种多槽位 CPU 计算机系统的工作方式,每个槽位的 CPU 有独立的内存总线,多 CPU 槽位通过总线连接起来的 SMP 系统。由于每个槽位的 CPU 有独立的内存总线,因此可以获得更高的内存带宽,可以获得更好的性能.目前高端服务器一般都是支持 NUMA 方式。

Libhaisqlmalloc 对于 NUMA 的管理模式提供了一种新的工作模式。默认关闭 NUMA 内存绑定.原因是 NUMA 这种工作模式的机器总量比较少(对硬件有很高的要求),并且低版本的 Linux 操作系统于 NUMA 的支持不完善,默认启用 NUMA 可能会导致一些版本下崩溃, 因此需要用户修改配置选项后才能启用 NUMA 相关功能.相关配置参数:见 /etc/libhaisqlmalloc.conf 中 bool_enable_numa_memory_bind 的说明。

尝试启用 NUMA 前建议首先请检查 numactl -s 的显示结果,下面是不支持 NUMA node 的情况,只有一个 NUMA node, 这种就不需要打开 NUMA 内存绑定功能。

numactl -s

policy: default

preferred node: current

physcpubind: 0 1 2 3 4 5

cpubind: 0

nodebind: 0

membind: 0

只有多个 NUMA node 节点存在的情况下,才有启用 NUMA 绑定的价值。对于支持 NUMA 的情况,如果 bool_enable_numa_memory_bind 配置为允许 NUMA 内存绑定,那么就会执行下列操作:当某个线程 malloc() 时,线程优先在当前申请内存的 CPU 所在的 NUMA node(如果当前 NUMA node 没有内存条,将选相邻 NUMA node)上分配内存;如果检测到当前优先 NUMA node 上没有足够的可用内存,将不使用线程优先分配策略,此时,由操作系统配置的默认 NUMA 分配策略分配内存.这种 NUMA node 自动绑定方式,在很多场景下会提供比 Linux 操作系统自带的 libnuma 库有更灵活更高性能的内存分配策略.因为这种分配策略是基于工作线程所在的 NUMA node 优先的分配策略,而传统的 libnuma 库是基于进程的 NUMA 分配策略.在内存足够并且用量均衡的理想情况下,每个 CPU 上跑的线程都是在当前 NUMA node 下申请的内存,不会出现线程在 node A 上跑,使用的却是 node B 上的内存的情况。

在目前常用的 Linux4.X 版本下,NUMA 的相关系统调用还不能彻底安全支持 Hugepage2M,因此,Libhaisqlmalloc 库关闭了 Hugepage2M 的 NUMA 内存绑定.就是说当前的分配策略是:当申请的 2M 块是 Hugepage 的时候,将不执行 NUMA node 内存绑定.只有申请的是常规 Page 内存,并且配置启用 NUMA 内存绑定功能,才会优先执行基于线程的 NUMA node 内存绑定,失败将执行操作系统默认 NUMA 策略。

  1. 更安全的低碎片的系统调用:

更安全的系统调用:LibLibhaisqlmalloc 库内部在申请内存和释放内存的时候,只使用 mmap / munmap,没有使用安全性较差的 madvice 函数(著名的脏牛漏洞就是利用 madvise(map, XXX, MADV_DONTNEED) 实现入侵,Facebook jemalloc 等库都有调用),so 文件初始化成功后没有 sbrk 和 brk 申请释放内存(so 文件初始化成功前也只存在一次 sbrk 调用),有更好的适应性,有利于操作系统进行页面归集整理,减少操作系统的内部碎片。

超低的操作系统碎片:LibLibhaisqlmalloc 库管理内存范围是 1Byte—-2MiB, 管理范围比其它内存分配库高一个数量级 (传统内存分配库一般只负责管理 128KiB 以下的大小),所有的内存申请都是将 2MiB 的大块内存切成小内存实现内存分配。LibLibhaisqlmalloc 库对操作系统的内存申请除了管理内存申请有偶然个别少量 64KiB 的申请外,其他内存申请都是 2MiB 或者 N*2MiB,对操作系统的内存管理非常友好,操作系统的碎片率很低,提高了操作系统的整体工作效率。

  1. 默认提供完全的线程安全性和数据同步保证:

LibLibhaisqlmalloc 库内部默认提供了完整的线程安全性保证,对应用程序没有任何同步或者线程安全性的要求。目前很多传统内存库不包含此功能点。LibLibhaisqlmalloc 库设计思路是以牺牲性能表现为代价,以 CPU 所有顺序性保证均不存在的严苛的数据存取条件,以严格的多线程编程规范,换取完整彻底的线程安全性和数据同步保证,具备优秀的 CPU 移植性。LibLibhaisqlmalloc 库在多线程下很稳定,代价就是在一些场景下降低了性能。

由于 LibLibhaisqlmalloc 库使用的 C++ 库基础算法非常快,具备极高的性能,更容易暴露出多线程冲突和数据同步问题,因此 LibLibhaisqlmalloc 库对应用程序没有任何同步要求,对库自身有严格的线程安全和数据同步要求,代码设计时假定 Intel X86 系列 CPU 的所有顺序性的保证均不存在,按照最苛刻的 CPU 顺序性进行代码设计(好处是方便今后移植到其它型号的 CPU 上),malloc / free 内部过程大量频繁的使用同步原语 (内存屏障 sfence / mfence / lfence / atomic / mutex 等),更加频繁的同步,以及大量的检测代码,降低了性能表现,但是分布在各个 CPU 的 cache 数据也更加同步,也顺便提升了使用 LibLibhaisqlmalloc 库的应用程序的整体多线程稳定性,提供了更多的线程安全保证。

LibLibhaisqlmalloc 库在多线程下很稳定,实现了很多额外(超过多线程编程规范要求)的线程安全和数据同步保证。C++ 多线程编程一直是程序员容易犯错误的难点。LibLibhaisqlmalloc 库为了避免多线程 BUG,设计代码时采用了一种防御性编程规范,以解决此难题。宁可在一些场景下降低性能表现,也要预防多线程 BUG。

1)在设计多线程代码时,按照最苛刻的 CPU 顺序性进行代码设计。假定 Intel X86 系列 CPU 的所有顺序性的保证均不存在,方便移植到各种 CPU 下。

2)大量的检测。假定所有的线程同步措施都有漏洞,封装检测代码,只要能够检测的都设法进行检测(例如 std::mutex 锁也封装了一层复杂的检测代码),处处检测,步步设防。

3)任何有疑点的数据存取,均使用同步原语,宁可多用,不可放过,更频繁的使用内存屏障。

4)使用 mutex 锁,不使用 spin_lock 锁。

5)外部调用没有把握时,使用 mutex 锁进行保护(一部分操作系统的系统调用,因为对旧 Linux 版本没有把握,也使用了 mutex 锁进行保护,超过了多线程编程规范要求)。

6)使用更高并发度的 wait_free 的无锁无等待架构,基于 cas 指令的 lock_free 无锁架构只允许用于并发冲突较小的场合(减少 cas 指令的广播风暴)。

7)所有需要锁保护的变量或者对象,全部在命名的最前面标记 locking_,表示提醒程序员这个变量或对象需要锁的额外保护。

下面举 2 个额外(超过多线程编程规范要求)的线程安全保证的例子进行说明:

范例 1:malloc 分配的指针都有对应的 cookie 数据,用于表示指针的内部数据结构,包括线程相关信息和大小类型等,指针有可能被跨线程使用。在跨线程使用指针的过程中,有可能出现应用程序跨线程使用指针没有使用同步原语(例如锁 / 原子量 / 内存屏障等)(这其实是线程不安全的错误用法),此时没有内部同步原语的传统方案有很小的概率会造成 cookie 数据没有被同步到其他 CPU 上。为了保证在任何情况下都能正确处理,不依赖应用代码提供同步原语,即使应用程序没有按照多线程编程规范要求写代码,LibLibhaisqlmalloc 库也可以正常处理,库内部默认提供了线程间的数据写同步原语(内存屏障 sfence),实现了 malloc 指针相关线程 cookie 数据(里面有 free 时用到的 thread_id)的写同步和其它更多措施保证, 保证在 malloc 返回结果前将指针的线程相关数据已经同步到所有 CPU。free 时可以读到准确的申请内存的线程 thread_id,准确判断是否是跨线程释放,实现安全释放。测试代码的最后有专门的一段代码测试跨线程非同步释放,可以看到虽然 LibLibhaisqlmalloc 库极快,但是也可以安全可靠的处理没有同步原语的跨线程释放。

范例 2:LibLibhaisqlmalloc 库在进入 free 函数后立即执行一次写同步(内存屏障 sfence),保证释放内存前所有与这个指针相关的写操作都已经完成,虽然理论上不存在单线程乱序执行 BUG,虽然 Intel X86 系列 CPU 有很多的顺序性的保证,理论上可以避免使用一些同步原语(尤其是不涉及跨线程释放的普通场景),LibLibhaisqlmalloc 库假定假定这些 CPU 的执行顺序性保证根本不存在,在代码中增加了大量额外的同步原语的保护,大幅度降低了性能表现,只是为了预防多线程 BUG,实践多线程防御性编程。

  1. 现代 C++ 是高性能的关键技术:

Libhaisqlmalloc 库使用了一些高性能组件,代码模块是用 C++ 写的,经过反复测试和调整,具备较高的性能表现。软件工程的实践中,很多的性能提升,来自于现代 C++ 技术。计算机从 1970 年至今只有 50 年的发展时间,最近一二十年来计算机的硬件技术发生了巨大的进步,软件追随硬件而变,新一代 C++ 软件充分发挥硬件新特性,发挥硬件的各种潜力(例如 CPU 多发射各种新指令集各种多线程高并发友好的新的数据结构与算法),时代变了,因此,以前写的古老的 C/C++ 库用最新的现代 C++ 技术重写一遍,很多都可以获得大幅度的性能提升,这是时代的红利,只是需要投入人力资源投入时间去做而已,just do it。

下面举 2 个现代 C++ 库的例子:

范例 1:跨线程内存释放是一种常见的场景,传统算法是使用传统锁保护下的队列。跨线程内存释放是一个非常典型的多生产者单消费者 MPSC 的工作场景, Libhaisqlmalloc 库使用了一种多生产者单消费者 MPSC 无锁队列 C++ 模板库,库代码大约数千行代码,也是完全线程安全(超过多线程编程规范要求)的,应用代码几行就实现了更高性能的解决方案,Libhaisqlmalloc 库使用的是一种自研的经过多线程防御性编程加固过的 C++ 模板库,性能一般(采用多线程防御性编程,大幅度降低了性能表现),但也比传统方案快 N 倍,采用最常用的 Ring Buffer 技术方案实现。

测试代码中有专门的一段代码测试跨线程释放,可以测试发现线程数 N=1-8 之间,性能是逐步提升的,超过 8 个线程后性能略有下降。现代 C++ 的无锁队列可以提供更好的性能表现,更低的延迟时间。Libhaisqlmalloc 库的跨线程内存释放比传统旧方案提升了数倍。

范例 2:重写了一部分常用 std 库组件,提供了更好的性能。以 vector 为例说明如下:

下面这种是内存库中的一个 class 中的一个 vector 对象, 把 C++ 的 vector , unique ptr, allocator 等组件都全部重写了,实现更多种类的组件,实现 std 库扩展,提升了性能,提供了各种无锁或锁冲突少的组件,实现高性能。一行代码定义一个非常高效的 vector 容器,如下所示:

vector_allocator, Memory_allocator > d_vt_up_normal;

这是一个装 unique_ptr 的 vector 容器,其中:Memory_allocator 是自定义实现的一组按 CPU 编号的多组页面对齐的内存分配组件,由于是 per CPU 的,按 CPU 分区,所以内部几乎是无锁的,这里作为 vector_allocator 的内存分配器,Malloc_page_part_cpu/Realloc_page_part_cpu/Free_page_part_cpu 这些都是函数对象,可以随时定义,随时使用,灵活组合和定义,比 std::allocator 的定义灵活的多。预先定义了一些常用的 Memory_allocator,都是基于函数对象的实现,非常灵活,函数对象调用起来完全没有任何额外开销。Memory_pool_without_lock_128byte 是一个线程内部的内存分配池,彻底无锁,只能分配 128Byte, 这里作为 unique_ptr 的内存分配器,性能极高。

另外由于使用了比较新的 C++ 算法,重写 vector 后一些常用函数的性能比 std 库有大幅提升,例如删除 / push_back 等,另外提供了一些更高性能的扩展函数,例如乱序删除等,性能提升了 N 倍。

  1. LibLibhaisqlmalloc 库的性能测试:

LibLibhaisqlmalloc 库是一个整体优秀的库,LibLibhaisqlmalloc 库是一个优秀的内存分配库,具备很高的稳定性,提升大型应用程序的性能,降低内存浪费和碎片,更有效使用内存。LibLibhaisqlmalloc 库使用了大量类似于 Intel dpdk / mpdk / spdk 的高性能 C++ 技术(这些库对比同领域的旧传统方案都是几个数量级的性能提升,网上有大量的公开资料和说明),因此,LibLibhaisqlmalloc 库在 “所有大小的内存申请和释放” 的性能也比几十年前设计的常见内存库 Linux ptmalloc 库 / Google tcmalloc 库 / Facebook jemalloc 等库更快。使用最新 C++ 软件技术重新写了一个可测试可下载可运行的新的内存分配库,只是投入研发资源做了一件平常的 C++ 库软件的开发工作,国内很少做基础库的研发,因为很难赚钱。

LibLibhaisqlmalloc 库不是一个单纯以高性能为目标的内存分配库,是一个以整体优秀为目标的内存分配库。代码的健壮性,内存的低浪费和低碎片,线程的安全,数据的同步等因素,都比性能更重要,程序设计时有大量的设计目标都是以大幅度牺牲性能为代价,以放弃性能表现来换取更重要的其它方面的技术指标,实现整体更加优秀。LibLibhaisqlmalloc 库对外提供二进制的动态链接库 so 文件,动态链接库的函数接口的固有开销比较大,因此,最终性能表现大打折扣。即使这样,全新设计的 LibLibhaisqlmalloc 库的性能也比传统内存分配库有一两个数量级的性能提升,说明传统内存分配库的方案已经过时了,是即将淘汰的软件。

LibLibhaisqlmalloc 库的架构思路,设计语言,碎片设计等都更接近 Google tcmalloc 库的理念,都是 C++, 都是 thread_local cache 的,都是低碎片设计,因此主要与 Google tcmalloc 库进行对比。

LibLibhaisqlmalloc 库加权平均性能比传统库 Google tcmalloc 库快了大约一个数量级(大约 14 倍,参见附录按照小内存更频繁,大内存更少的原则设计的测试代码默认执行的结果, 参见 test_loop() in loop 1 use nanoseconds=387419862 这一行的测试结果), 其中 4KiB 以上内存申请的性能提升较大,4KiB 的申请比 Google tcmalloc 库快了两个数量级大约 140 倍,测试方案是一个动态链接的外部接口的性能测试(so 接口开销较大,是重要影响因素之一),测试代码本身还有额外的开销(代码中有测试,但是没有扣除这块的额外开销,大约 5%),真实的性能还要更高一些。

具体的测试性能的细节可以参考附录二给出的测试情况,可以使用附录三给出的方法编译测试代码,调整测试代码的行参数进行更多的更详细的对比测试。(测试软件的用法请见第五章第一节的说明。)

配套的性能测试源码也是我们(乌鲁木齐云山云海信息有限责任公司)开发的,要求编译器支持 C++14,开源协议是 Apache2 协议,商业友好,已经在开源中国的网站上开放源码和二进制执行程序,具体的源代码可以从下列地址下载:郭忠明 / test_malloc_use_so

具体的二进制 so 文件和用户手册可以从下列地址下载:下载中心 / 产品下载_乌鲁木齐云山云海信息技术有限责任公司

郭忠明 / libhaisqlmalloc

LibLibhaisqlmalloc 库自身是一个商业软件,完全由乌鲁木齐云山云海信息技术有限责任公司自主研发,使用需要支付费用,不是开源软件。只对用户提供二进制的动态链接库 so 文件。目前只支持 Linux 操作系统,今后有足够需求的话会移植到其它操作系统,也会支持更多的 CPU 类型。

第六章 内存库的对外函数接口

LibLibhaisqlmalloc 库目前接管了下列内存相关的函数接口实现内存分配,能够满足多数场景下的内存分配的需求,也可以兼容多数 Linux 应用。

内存库的所有对外接口:

对外的 C 函数接口支持如下:

void* malloc( size_t sizet_size_in ) __THROW

void free( void* ptr_void_in ) __THROW

void realloc( void ptr_void_in, size_t sizet_size_in ) __THROW

void reallocarray( void ptr_void_in, size_t sizet_nmemb_in, size_t sizet_size_in ) __THROW

void* calloc( size_t n, size_t sizet_size_in ) __THROW

void cfree( void* ptr_void_in ) __THROW

void* valloc( size_t sizet_size_in ) __THROW

void* pvalloc( size_t sizet_size_in ) __THROW

void* aligned_alloc( size_t sizet_alignment_in, size_t sizet_size_in ) __THROW

void* memalign( size_t sizet_alignment_in, size_t sizet_size_in ) __THROW

int posix_memalign( void** ptr_ptr_void_in, size_t sizet_alignment_in, size_t sizet_size_in ) __THROW

void malloc_stats(void) __THROW

int mallopt( int int_key, int int_value ) __THROW

size_t malloc_size( void* p ) __THROW

int malloc_trim( size_t sizet_pad_in ) __THROW

size_t malloc_usable_size( void* ptr_void_in ) __THROW

相关主要函数简单介绍:

函数原型:void free(void* p) 函数功能:释放指针 p 指向的内存块 p 必须是由 malloc、calloc 或 realloc 返回的指针

calloc:申请一段内存空间,并将这段内存空间初始化为 0

realloc:改变之前申请的一段内存块的大小

malloc 函数接口默认与 Linux 的接口兼容,但是也有区别,说明如下:

头文件: #include

函数原型:void* malloc(size_t size)

返回值:成功:返回一个指针,该指针指向一块至少有 size 个字节的内存块。如果 size == 0,返回一个 8 字节空间的内存指针。

失败:将尝试执行 std::new_handler 所指的函数,尝试释放保留内存,最多连续尝试 8 次,如果有空间可用,则正常返回,如果失败,将执行 std::terminate()。

这种设计模式与传统的 C 语言的内存分配库的设计不一样,更加 C++,具体用法可以参考 std::new_handler 的相关说明。

这种扩展的 C++ 模式可以从源头上提升应用程序的性能。原因在于:

1)如果内存不足,从保留内存中释放一部分,并且应用程序能够迅速收到通知,准备执行进程安全退出。如果没有定义 std::new_handler,将会执行 std::terminate() 退出。

2)应用程序在每次 malloc/new/realloc 后,应用程序的代码中可以不用检查返回的指针是否为空,所有返回值一定不是 nullptr,这样就大幅度简化了内存申请 / 对象创建的流程,简化了代码设计,提升了性能。由于也不需要用 catch thow 捕捉内存不足的情况,所有的函数都可以设置为 noexecpt(可以提升大约 5% 的应用函数性能)。这种先进的高性能内存申请模式,更适合从头开发的高性能应用程序的开发,因为只有在代码中大幅度简化流程,才可以真正提升性能。

对外的 C++ 函数接口支持如下:

void* operator new( size_t sizet_size_in ) __THROW

void* operator new __THROW

void* operator new( size_t sizet_size_in, const std::nothrow_t ¬hrowt_in ) noexcept

void* operator new noexcept

void* operator new( size_t sizet_size_in, std::align_val_t alignt_in ) __THROW

void* operator new __THROW

void* operator new( size_t sizet_size_in, std::align_val_t alignt_in, const std::nothrow_t ¬hrowt_in ) noexcept

void* operator new noexcept

void operator delete( void* ptr_void_in ) __THROW

void operator delete __THROW

void operator delete(void* p, const std::nothrow_t ¬hrowt_in ) noexcept

void operator delete noexcept

void operator delete( void* ptr_void_in, std::align_val_t alignt_in ) __THROW

void operator delete __THROW

void operator delete( void* ptr_void_in, std::align_val_t alignt_in, const std::nothrow_t ¬hrowt_in ) noexcept

void operator delete noexcept

void operator delete( void* ptr_void_in, size_t sizet_size_in ) __THROW

void operator delete __THROW

void operator delete( void* ptr_void_in, size_t sizet_size_in, std::align_val_t alignt_in ) __THROW

void operator delete __THROW

唯一的扩展函数:

void malloc_version(void) noexcept;

用于显示软件的版权信息。

内存库的兼容性需要做出抉择:是否为了兼容越界 Bug 多分配几个字节。

Linux 早期系统中一些应用代码存在越界 bug, 判断是否要重新申请更大块的内存的判断出现了错误,为了避免这种低级 bug, 保证有越界 bug 的程序也可以稳定运行,工程上的内存库的临时解决办法就是 malloc 多分配几个字节,不用改一行代码,就可以解决很多越界 bug, 这个是工程上妥协的手段。

现在 Linux 的各种程序的代码质量已经很高了,很少有越界 bug。因此,LibLibhaisqlmalloc 库按实际内存申请量分配内存,减少了内存的浪费。

下面是 Linux 操作系统默认的 Ptmalloc 的 malloc() 后执行 malloc_usable_size() 的返回结果,可以看到每个内存申请都多分配了几个字节

Log: check_malloc_usable_size() malloc() malloc_size=8, malloc_usable_size=24

Log: check_malloc_usable_size() malloc() malloc_size=16, malloc_usable_size=24

Log: check_malloc_usable_size() malloc() malloc_size=24, malloc_usable_size=24

Log: check_malloc_usable_size() malloc() malloc_size=16, malloc_usable_size=24

Log: check_malloc_usable_size() malloc() malloc_size=32, malloc_usable_size=40

Log: check_malloc_usable_size() malloc() malloc_size=48, malloc_usable_size=56

Log: check_malloc_usable_size() malloc() malloc_size=64, malloc_usable_size=72

Log: check_malloc_usable_size() malloc() malloc_size=80, malloc_usable_size=88

Log: check_malloc_usable_size() malloc() malloc_size=96, malloc_usable_size=104

Log: check_malloc_usable_size() malloc() malloc_size=112, malloc_usable_size=120

Log: check_malloc_usable_size() malloc() malloc_size=128, malloc_usable_size=136

Log: check_malloc_usable_size() malloc() malloc_size=144, malloc_usable_size=152

Log: check_malloc_usable_size() malloc() malloc_size=160, malloc_usable_size=168

Log: check_malloc_usable_size() malloc() malloc_size=176, malloc_usable_size=184

Log: check_malloc_usable_size() malloc() malloc_size=192, malloc_usable_size=200

Log: check_malloc_usable_size() malloc() malloc_size=208, malloc_usable_size=216

Log: check_malloc_usable_size() malloc() malloc_size=224, malloc_usable_size=232

Log: check_malloc_usable_size() malloc() malloc_size=240, malloc_usable_size=248

下面是 LibLibhaisqlmalloc 库的 malloc() 后执行 malloc_usable_size() 的返回结果,可以看到每个内存申请都刚好适配

类似的 Google tcmalloc 也是按实际内存申请量分配内存,每个内存申请都刚好适配。

LibLibhaisqlmalloc 库支持 int malloc_trim( size_t sizet_pad_in )

使用时参数 sizet_pad_in 在函数内部其实并没有使用,这个限制值不会起任何作用。LibLibhaisqlmalloc 库中该函数执行后将尽最大可能释放所有可以释放的内存,。

LibLibhaisqlmalloc 库提供了显示内部分配情况的接口 malloc_stats,与其他内存库的调用方式兼容。

用法见下面的定义:

include

void malloc_stats(void);

LibLibhaisqlmalloc 库中的内存分配是非常复杂的一个体系,这个体系首先区分了两大类内存,一种是自身消耗的内存,用于管理数据,这种内存我们称为 Metadata, 第二种是给用户分配的内存,这种内存以 NUMA node 和 Thread Local 为核心,从操作系统申请的块大小是 2M 块,再切成小内存分配给用户。下面是一个 malloc_stats() 调用后的显示案例:

Libhaisqlmalloc os_byte=(14.566406 MiB), os_page_byte=(8.566406 MiB), os_hugepage_2M_byte=(6 MiB)

Libhaisqlmalloc shared_page all_byte=(384 KiB), use_byte=(111.3125 KiB), free_byte=(272.6875 KiB), free_rate=71%

Libhaisqlmalloc metadata_shared_tiny_block all_byte=(64 KiB), all_bit=512, use_bit=31, free_bit=481, free_rate=93%

Libhaisqlmalloc metadata_cpu_tiny_block cpu=0, all_byte=(64 KiB), all_bit=512, use_bit=6, free_bit=506, free_rate=98%

Libhaisqlmalloc metadata_cpu_tiny_block cpu=1, all_byte=(64 KiB), all_bit=512, use_bit=4, free_bit=508, free_rate=99%

Libhaisqlmalloc metadata_cpu_tiny_block cpu=3, all_byte=(64 KiB), all_bit=512, use_bit=2, free_bit=510, free_rate=99%

Libhaisqlmalloc metadata_cpu_tiny_block cpu=4, all_byte=(64 KiB), all_bit=512, use_bit=2, free_bit=510, free_rate=99%

Libhaisqlmalloc metadata_cpu_tiny_block cpu=5, all_byte=(64 KiB), all_bit=512, use_bit=2, free_bit=510, free_rate=99%

Libhaisqlmalloc metadata_cpu_page_block cpu=0, all_byte=(2 MiB), byte_page=(2 MiB), all_bit=512, use_bit=353, free_bit=159, free_rate=31%

Libhaisqlmalloc metadata_cpu_page_block cpu=1, all_byte=(2 MiB), byte_hugepage2M=(2 MiB), all_bit=512, use_bit=220, free_bit=292, free_rate=57%

Libhaisqlmalloc metadata_cpu_page_block cpu=2, all_byte=(2 MiB), byte_page=(2 MiB), all_bit=512, use_bit=1, free_bit=511, free_rate=99%

Libhaisqlmalloc metadata_cpu_page_block cpu=3, all_byte=(2 MiB), byte_hugepage2M=(2 MiB), all_bit=512, use_bit=127, free_bit=385, free_rate=75%

Libhaisqlmalloc metadata_cpu_page_block cpu=4, all_byte=(2 MiB), byte_page=(2 MiB), all_bit=512, use_bit=124, free_bit=388, free_rate=75%

Libhaisqlmalloc metadata_cpu_page_block cpu=5, all_byte=(2 MiB), byte_hugepage2M=(2 MiB), all_bit=512, use_bit=123, free_bit=389, free_rate=75%

Libhaisqlmalloc numa_node_count=1

libhaisqlmalloc thread_local_data vt_all_size=8, vt_wait_reuse_size=7

libhaisqlmalloc thread_id=00007fd0e1284740, ptr_thread_local=00007fd0dfc2e000, all_byte=(2 MiB), page_byte=(2 MiB), all_bit=512, use_bit=216, free_bit=296, free_rate=57%

在上面的例子中,os_byte 表示向操作系统 (OS) 申请的内存总量,os_page_byte 表示向操作系统 (OS) 申请的 4K Page 的内存总量,os_hugepage_2M_byte 表示向操作系统 (OS) 申请的 Hugepage2M 的内存总量。

shared_page 表示共享的一块内存(默认大小为 384KiB),这块内存是紧急内存块,是 so 文件初始化尚未完成前就开始执行 malloc 申请的内存,主要用于 so 文件还没有初始化完毕,以及 so 文件已经开始析构的场景下,也叫紧急分配内存块,这部分内存非常特殊。

这部分内存是唯一使用 sbrk 方式一次性分配,并且分配后不会自动收缩的内存,这部分内存在常规场景下只需要不到 128KiB 就足够满足需求,在 X Windows 的应用下就需要配置更多,因此,统一配置为 384KiB。

all_byte 表示全部申请内存, use_byte 表示已经使用的内存,free_byte 表示空闲的内存,free_rate 表示空闲百分比。

Metadata 用于 LibLibhaisqlmalloc 库的内部管理数据,有三种类型:

类型一 metadata_shared_tiny_block 是多 CPU 共享的用来分配 128 字节大小的管理空间。此数据结构自身的 128 字节来自于从 shared_page 中的分配。

类型二 metadata_cpu_tiny_block 是每 CPU 独立的用来分配 128 字节大小的管理空间,此数据结构自身的 128 字节来自于类型一。

类型三 metadata_cpu_page_block 是每 CPU 独立的用来分配按页面对齐的管理空间,此数据结构自身的 128 字节来自于类型二。

numa_node_count 表示 NUMA node 的数量

numa_node_2m_block_queue 表示进程单个 NUMA node 下所有 CPU 共享的2M 块的 MPMC 无锁队列

vt_all_size 表示 thread_local 容器(含备用数据)的数据量是多少

vt_wait_reuse_size 表示 thread_local 备用容器的的数据量是多少 (备用数据的相关配置参数:见 /etc/libhaisqlmalloc.conf 中 uint_vt_thread_local_wait_reuse_limit_size 的说明)。

thread_id 是 c/c++ 获得的 thread id 值,是一个与线程相关的地址值,等效于 std::thread::thread_id, 当 thread_id 显示为零时表示该数据结构对应的线程已经退出,但是有内存没有释放(并不表示出现了内存泄露,有可能一些指针所指的对象正由其他工作线程持有),对应的也有一些少量的 2M 块没有释放掉。这些 thread_local 数据结构是可以被反复复用的,因此,有一个 thread_local 的池子用于复用。

ptr_thread_local 表示指向 thread_local 的指针。

all_byte 表示所有字节数。 page_byte 表示常规页面的字节数。

all_bit 表示所有的 Bitmap 的 bit 数。

use_bit 表示已经使用的 bit 数。

free_bit 表示空闲的 bit 数。

free_rate 表示空闲的百分比。

第七章 内存库的购买和备份恢复

Libhaisqlmalloc 内存库是一个完全自研的非开源的内存分配库,花费了商业公司的大量开发时间进行开发和各种优化,使用了大量前沿现代 C++ 技术,研发了大量新的 C++ 模板库,使用了大量的新算法,自有知识产权的源码总行数高达 4.2 万行,代码量很大,代码量超过很多常见内存分配库的规模,性能极高,技术研发周期很长,研发时间数年,研发成本很高。

考虑软件市场的实际情况,该专业软件定价也很低,希望所有用户能够支持一下中国基础软件的研发。获取的收入将继续投入到其它重要基础软件的研发。

下面是非注册版运行时的显示内容

Notice: Libhaimalloc Version 1.01 Copyright by 乌鲁木齐云山云海信息技术有限责任公司 Publish Date: 2021.01

Notice: Basic version. max_support_numa_node=8, max_support_cpu=136

Notice: unregister version. Please buy the software ( libhaisqlmalloc.so ) and register it.

Notice: link HaiSql

Notice: This software ( libhaisqlmalloc.so ) test time has 23 hours 15 minutes 11 seconds

购买链接是第 4 行的那个 http 链接。购买后将获取一个 libhaisqlmalloc.conf 的配置文件,这个配置文件放置到 /etc/libhaisqlmalloc.conf 或者 当前 libhaisqlmalloc.so 文件所在的目录下即可。用户可以修改此 libhaisqlmalloc.conf 文件中的其他内容,以便优化性能,见第四章配置文件和配置选项的说明。

配置文件的一个范例:

str_mac=00e07096a61a

str_device=enp2s0

ulong_copy_id=1605876329

uint_copy_id=156155068

uint_version=101

ulong_create_id=1101081971070163

ulong_verify_id=12739876336654934

这些配置内容主要是依据用户机器的网卡的 MAC 地址,so 文件自身参数生成的一系列编码, 标识了每一个 so 文件的拷贝,用于防盗版,每一份拷贝都有单独的验证码。

如果用户重新装了系统,从备份或者其他机器或者其他压缩格式中恢复的(例如同一台机器的同样 ext4 文件系统格式的),甚至可能是从其他网站中下载的,那么就需要重新注册和认证。此备份将会被认为是一个新的拷贝,将自动获得 724 小时的测试时间,作为测试版本运行(测试版本将会在每次使用 Libhaisqlmalloc 库时向 stderr 输出 NOTICE 版本信息),测试超过 724 小时将自动 abort 退出,会影响用户程序的正常使用,因此,从其他机器或者其他压缩格式中恢复备份后,请及时重新注册和认证。

已经付费的用户,在同样的机器上(通过网卡的 MAC 地址识别),默认自动获得 10 次重新注册的机会,可以直接获取新的验证码和新的 libhaisqlmalloc.conf 文件,直接下载下来覆盖掉旧配置文件即可。非注册版默认获得 724 小时的测试时间,超过 724 小时,运行将自动退出。

关于内存库的其他限制条件:

目前用户直接获取的 Libhaisqlmalloc 库都是基本版。

该版本默认限制是最大只支持 8 个 NUMA node(一般等于 CPU 槽位数,PC 和低端服务器只有一个默认的 NUMA node)。

该版本默认限制是最大只支持 128 个 CPU, 一般中低端服务器均低于 128 个 CPU。

对于超过 8 个 NUMA node 或超过 128 个 CPU 的需求,这些需要 ADVANCE VERSION, 请联系:乌鲁木齐云山云海信息技术有限责任公司

微信联系方式:微信 wlmqgzm。

邮件联系方式:haisql@sina.com.cn

对于大批量购买或者定制需求,也欢迎联系,谢谢您的支持。
https://zhuanlan.zhihu.com/p/352938740