当前位置: 首页 > article >正文

jemalloc 5.3.0的tsd模块的源码分析

一、背景

在主流的内存库里,jemalloc作为android 5.0-android 10.0的默认分配器肯定占用了非常重要的一席之地。jemalloc的低版本和高版本之间的差异特别大,低版本的诸多网上整理的总结,无论是在概念上和还是在结构体命名上在新版本中很多都找不到,而高版本尤其5.x.x版本就几乎没有现成的网上整理文档。这篇博客作为jemalloc 5.3.0专栏里的第一篇,后续会不断更新jemalloc 5.3.0版本的源码分析及实验对比,揭开jemalloc 5.3.0里的诸多实现上的细节。

这篇博客,我们会先介绍jemalloc 5.3.0里的tsd模块,为什么第一篇jemalloc 5.3.0的博客要介绍这个tsd模块呢?因为在jemalloc 5.3.0里,tsd模块算是一个基础组件模块,不先分析tsd模块,绕开它,会让源码分析困难重重。另外,tsd模块在实现上有不少编程上的技巧,不少技巧对于我们C++尤其C的开发人员也是非常值得借鉴的,尤其tsd模块里的若干宏定义和展开的实现,极简的优化了代码量,这种C的宏展开方式来达到的最终代码执行的效果,虽然从代码阅读上可能会有些晦涩,但是一旦掌握以后,它所带来的代码简化收益会相当可观,实现上可能会比替代用C++的大量的抽象和继承而定义的大量的冗余的类而言,从长期阅读观感上会更为极简。

我们会在第二章里先介绍jemalloc 5.3.0里的tsd模块的实现细节,其底层用到的glibc的tls机制细节可以参考之前的博客 线程局部存储tls的原理和使用-CSDN博客,然后在第三章里,我们对第二章里介绍的一些细节进行提炼和抽象,抽象出可以进行复用的编程上的技巧,作为我们一线开发人员长期编码上的一些参考。

二、jemalloc 5.3.0里的tsd模块的实现细节

我们先在 2.1 里介绍一下jemalloc 5.3.0的代码下载编译,并介绍个人在为了方便调试和调试分析的编译方式和进行jemalloc库的调试。在 2.2 里我们介绍tsd模块的用途和实现原理。

2.1 jemalloc 5.3.0的代码下载、编译和调试

2.1.1 代码下载和编译

jemalloc的github网址:

https://github.com/jemalloc/jemalloc

下载命令:

git clone https://github.com/jemalloc/jemalloc.git

cd jemalloc以后,执行如下命令切换到 5.3.0 版本:

git checkout 5.3.0

2.1.2 为了方便调试分析修改了一下源码和编译参数

关于tsd模块有一个宏定义的名字过长导致变量的名字过长,造成在通过vs2019进行ssh的gdb调试时,显示不出完整的变量名,造成代码分析的障碍,所以,临时为了调试方便,把该宏改短:

在tsd.h里,原始的宏定义如下:

临时改成(下面的名字可按照个人情况随意指定):

改完以后,先要执行自动生成编译用的文件的指令:

./autogen.sh

如果遇到如下错误:

则需要进行autoconf的安装:

apt-get update
apt-get install autoconf

安装完autoconf以后,重新执行./autogen.sh

在make之前,配置一下参数,使用O0,覆盖掉原来的O3:

./configure CFLAGS="-O0"

然后再执行make

如果遇到如下warning:

可以忽略,或者修改一下configure.ac文件里的ARFLAGS,下图是原始的内容:

修改成:

重新执行一遍上面的./autogen.sh ./configure xxx make clean;make -jx的流程以后,就不会遇到“ar: `u' 修饰符被忽略,因为 `D' 为默认(参见 `U')”的错误了。意思就是有了‘D’作为默认,ARFLAGS的‘u’就会被忽略,那就是不需要这个‘u’,去掉即可。

2.1.3 调试jemalloc库

经过上面的编译之后,生成物默认在jemalloc文件夹下的lib目录下,把它们拷贝到/usr/lib下:

然后我们参考 linux上对于so库的调试——包含通过vs2019远程ssh调试so库_vs2019 gdb调试-CSDN博客 这篇博客的方法进行远程ssh进行gdb调试jemalloc的库(博客里举的例子就是调试jemalloc库的例子)。

2.2 tsd模块的用途和实现原理

2.2.1 tsd模块借助的是glibc提供的tls机制

jemalloc 5.3.0里的tsd的意思是指Thread-Specific-Data,用的是之前的博客 线程局部存储tls的原理和使用-CSDN博客 提到的glibc的tls机制来实现的。jemalloc有关tsd的注释:

2.2.2 tsd模块实际用到的就4个文件

tsd模块的源文件就一个是src/tsd.c,头文件有多个:

但是对于x86_64 linux平台,根据tsd.h里下面这段根据编译选项来决定用那个头文件,而不用另外的几个头文件:

上图中,根据增加#err来确定,x86_64 linux平台,用的就是tsd_tls.h头文件。所以tsd模块里我们需要关注的头文件就只有下面这三个:

tsd.h

tsd_tls.h

tsd_types.h

所以,tsd模块在x86_64 linux平台实际用到的就4个文件:

tsd.c tsd.h tsd_tls.h tsd_types.h

2.2.3 tsd模块实现的核心是tsd.h文件,struct tsd_s按照TSD_DATA_SLOW、TSD_DATA_FAST、TSD_DATA_SLOWER定义不同的场景下用到的数据

在tsd.h的一开头的注释里有如下内容:

注释里清晰地表达了为了提高cache命中率,把不同场景下可能会用到的数据各自放到临近的区域,虽然TSD_DATA_SLOW、TSD_DATA_FAST、TSD_DATA_SLOWER用到的数据都定义在strcut tsd_s这个结构体里:

但是它们不同的path(fast-path或slow-path)用到的数据从定义的位置上都是连续的。

在tsd模块里的若干关键函数,如tsd_fetch_slow、tsd_state_set、tsd_add_nominal、tsd_remove_nominal等第一个入参tsd_t *tsd其实就是上图中的struct tsd_s这个结构体的指针:

上图中还有一个相关的定义是:

tsdn_s其实也是一样的struct tsd_s结构体的指针:

定义一个tsdn_t是为了和tsd_t来区分,tsdn_t是可以为NULL的,而tsd_t指针是由tls机制直接获取到的struct tsd_s数据的指针,如下图在tsd_tls.h里有定义:

而刚才说的tsd_fetch_slow、tsd_state_set、tsd_add_nominal、tsd_remove_nominal这些函数的第一个入参tsd_t *tsd实际上都是通过类似如下截图的函数tsd_get来获取到的:

所以,很显然它的地址是不可能是NULL的。相关的注释如下:

对于入参是tsdn_t *tsdn的函数如iallocztm等而言,传入TSDN_NULL和非NULL做区分可以用来表示特殊的含义,如下图:

2.2.4 详细分析一下TSD_DATA_SLOW、TSD_DATA_FAST、TSD_DATA_SLOWER这几个宏

在tsd模块的实现里,TSD_DATA_SLOW、TSD_DATA_FAST、TSD_DATA_SLOWER这三个宏可以说是关键。我们以TSD_DATA_SLOW宏为例,搜索后可以发现,它被反复的使用:

而且每次使用,它都有实际不同的含义:

我们分别来展开一下:

tsd.h里的第一处TSD_DATA_SLOW是在定义struct tsd_s这个结构体时:

TSD_DATA_SLOW展开是受#define O(n, t, nt)的影响的:

所以在struct tsd_s {的定义里,O(n, t, nt) 被定义成t TSD_MANGLE(n)

而TSD_MANGLE(n)被定义成(为了方便调试缩短了变量名,见 2.1.2 里的说明):

所以在struct tsd_s {的定义里就是声明了一下结构体里的成员,但是名字要加一个头。

再看第二处TSD_DATA_SLOW的使用,如下图,定义O是一个p_get_unsafe结尾的一个函数:

所以上面的TSD_DATA_SLOW展开就是定义了获取tsd_s结构体里的DATA_SLOW部分里的成员变量一个个的获取函数

再看第三处,和第二处差不多,是定义了获取tsd_s结构体里的DATA_SLOW部分里的成员变量一个个的带tsd的state的assert检查的函数(关于tsd的state的简要说明见 2.2.5 一节):

再看第四处,也和第二、第三处差不多,是定义了获取tsd_s结构体里的DATA_SLOW部分里的成员变量一个个的带入参检查的获取函数,因为入参是tsdn_t*,是可能是NULL的(在 2.2.3 里有说明):

第五处及以后就不一一展开了。

2.2.5 tsd的state的minimal和nominal

在这一章的最后,我们涉及一下tsd模块里的函数经常会碰到tsd的state有关的minimal和nominal的区别。

tsd里除了变量定义一块以外,还有一个tsd的state的状态维护逻辑。这篇博客先不涉及过深的状态差异上的细节,先讲提及最常见的tsd的state状态,即nominal状态。

tsd的state的定义在tsd.h里:

上图中红色框出的是最常见的nominal状态,已经初始化完了且是快速路径的是这个状态。

上面的三个状态0,1,2,到2为止,都是属于泛nominal状态,表示的是线程不处于创建和销毁时期而导致tsd“不完整”的状态。

nominal在jemalloc里的大致意思就是某种理想或者预期的分配方式,那对于内存分配,理想或者预期就是能分配效率比较快也就是快速路径的状态。

三、从tsd模块实现中可以借鉴的一些编码技巧

3.1 把不同场景高概率一起用到的数据放在一起连续的进行定义,从而增加cache的命中率

在上面 2.2.3 里讲到的tsd模块按照TSD_DATA_SLOW、TSD_DATA_FAST、TSD_DATA_SLOWER定义不同的数据块内容,虽然最终在一个结构体里,但是它们的定义是连续的,从而让数据所在的内存上的区域更容易临近,从而提到cache的命中率

3.2 通过define和undef宏定义里所依赖的宏,实现批量成员变量定义和批量函数定义

在上面的 2.2.4 一节里讲到,通过重新define和undef O(n, t, nt)来配合TSD_DATA_SLOW、TSD_DATA_FAST、TSD_DATA_SLOWER来实现批量的函数和成员变量的定义,这种宏操作可以用于简化一些重复冗余代码,方便统一化维护和修改

3.3 结构体指针的定义通过typedef不同的类型做代码直观上的轻度“解耦”,可用于体现一些入参范围的差异,如nullable和non-nullable

在 2.2.3 里我们讲到了 tsdn_t的定义是增加了NULL可能的tsd_t,我们可以借鉴这样的定义方式,来方便一些模块函数在参数传入需要做区分时的编程上的一定的解耦,方便后期的维护,也从代码里直观察觉到差异,避免参数各个场景下的误用和逻辑上的误判


http://www.kler.cn/a/523406.html

相关文章:

  • 【JavaWeb06】Tomcat基础入门:架构理解与基本配置指南
  • docker安装Redis:docker离线安装Redis、docker在线安装Redis、Redis镜像下载、Redis配置、Redis命令
  • C++ unordered_map和unordered_set的使用,哈希表的实现
  • wordpress代码结构解析
  • NPM 使用介绍
  • 2024年记 | 凛冬将至
  • 关于存储磁盘固件版本:打破版本一致性迷思
  • Python 函数魔法书:基础、范例、避坑、测验与项目实战
  • 大模型概述
  • 第一个3D程序!
  • 在虚拟机里运行frida-server以实现对虚拟机目标软件的监测和修改参数(一)(android Google Api 35高版本版)
  • 借DeepSeek-R1东风,开启创业新机遇
  • nosql mysql的区别
  • SQL server 数据库使用整理
  • 实时数据处理与模型推理:利用 Spring AI 实现对数据的推理与分析
  • 29. 【.NET 8 实战--孢子记账--从单体到微服务】--项目发布
  • 如何保证缓存与数据库的数据一致性?
  • 《多线程基础之条件变量》
  • @RestControllerAdvice 的作用
  • 【信息系统项目管理师-选择真题】2010下半年综合知识答案和详解
  • C#面试常考随笔5:简单讲述下反射
  • 腾讯云开发提供免费GPU服务
  • 大数运算:整数、小数的加减乘除与取余乘方(c++实现)
  • 我们需要有哪些知识体系,知识体系里面要有什么哪些内容?
  • 面试被问的一些问题汇总(持续更新)
  • Python帝王學集成-母稿