jemalloc 3.6.0源码详解—[1]Arena

来源:互联网 发布:2017中甲数据 编辑:程序博客网 时间:2024/05/29 11:46

转载自:vector03

2.2 Arena (arena_t)

如前所述, Arena是jemalloc中最大或者说最顶层的基础结构. 这个概念其实上是针对”对称多处理机(SMP)”产生的. 在SMP中, 导致性能劣化的一个重要原因在于”false sharing”导致cache-line失效.

为了解决cache-line共享问题, 同时保证更少的内部碎片(internal fragmentation), jemalloc使用了arena.

2.2.1 CPU Cache-Line

现代处理器为了解决内存总线吞吐量的瓶颈使用了内部cache技术. 尽管cache的工作机制很复杂, 且对外透明, 但在编程上, 还是有必要了解cache的基本性质.

Cache相当于嵌入到cpu内部的一组内存单元, 速度是主存的N倍, 但造价很高, 因此一般容量很小. 有些cpu设计了容量逐级逐渐增大的多级cache, 但速度逐级递减. 多级处理更复杂, 但原理类似, 为了简化, 仅讨论L1 data cache.

cache同主存进行数据交换有一个最小粒度, 称为cache-line, 通常这个值为64. 例如,在一个ILP32的机器上, 一次cache交换可以读写连续16个int型数据. 因此当访问数组#0元素时, 后面15个元素也被同时”免费”读到了cache中, 这对于数组的连续访问是非常有利的. 然而这种免费加载不总是会带来好处, 有时候甚至起到反效果, 所谓”false sharing”.

试想两个线程A和B分别执行在不同的cpu核心中,并分别操作各自上下文中的变量x和y.如果因为某种原因(比如x, y可能位于同一个class内部, 或者分别是数组中的两个相邻元素), 两者位于相同的cache-line中, 则在两个core的L1 cache里都存在x和y的副本. 倘若线程A修改了x的值, 就会导致在B中的x与A中看到的不一致. 尽管这个变量x对B可能毫无用处, 但cpu为了保证前后的正确和一致性, 只能判定core #1的cache失效. 因此core #0必须将cache-line回写到主存, 然后core #1再重新加载cache-line, 反之亦然. 如果恰好两个线程交替操作同一cache-line中的数据, 将对cache将造成极大的损害, 导致严重的性能退化.

这里写图片描述

说到底, 从程序的角度看, 变量是独立的地址单元, 但在CPU看来则是以cache-line为整体的单元. 单独的变量竞争可以在代码中增加同步来解决, 而cache-line的竞争是透明的, 不可控的, 只能被动由CPU仲裁. 这种观察角度和处理方式的区别, 正是false sharing的根源.

2.2.2 Arena原理

回到memory allocator的话题上. 对于一个多线程+多CPU核心的运行环境, 传统分配器中大量开销被浪费在lock contention和false sharing上, 随着线程数量和核心数量增多, 这种分配压力将越来越大.

针对多线程, 一种解决方法是将一把global lock分散成很多与线程相关的lock. 而针对多核心, 则要尽量把不同线程下分配的内存隔离开, 避免不同线程使用同一个cache-line的情况. 按照上面的思路, 一个较好的实现方式就是引入arena.

这里写图片描述

jemalloc将内存划分成若干数量的arenas, 线程最终会与某一个arena绑定. 比如上图中的threadA和B就分别绑定到arena #1和arena #3上. 由于两个arena在地址空间上几乎不存在任何联系, 就可以在无锁的状态下完成分配. 同样由于空间不连续, 落到同一个cache-line中的几率也很小, 保证了各自独立.

由于arena的数量有限, 因此不能保证所有线程都能独占arena, 比如, 图中threadA和C就都绑定到了arena1上. 分享同一个arena的所有线程, 由该arena内部的lock保持同步.

jemalloc将arena保存到一个数组中, 该数组全局记录了所有arenas,

arena_t   **arenas;

事实上, 该数组是动态分配的, arenas仅仅是一个数组指针. 默认情况下arenas数组的长度与如下变量相关,

unsigned    narenas_total;unsigned    narenas_auto;size_t        opt_narenas = 0;

而它们又与当前cpu核心数量相关. 核心数量记录在另外一个全局变量ncpus里,

unsigned    ncpus;

如果ncpus等于1, 则有且仅有一个arena, 如果大于1, 则默认arenas的数量为ncpus的四倍. 即双核下8个arena, 四核下16个arena, 依此类推.

(gdb) p ncpus $20 = 4(gdb) p narenas_total$21 = 16

jemalloc变体很多, 不同变体对arenas的数量有所调整, 比如firefox中arena固定为1, 而android被限定为最大不超过2. 这个限制被写到android jemalloc的mk文件中.

2.2.3 choose_arena

最早引入arena并非由jemalloc首创, 但早期线程与arena绑定是通过hash线程id实现的, 相对来说随机性比较强. jemalloc改进了绑定的算法, 使之更加科学合理.

jemalloc中线程与arena绑定由函数choose_arena完成, 被绑定的arena记录在该线程的tls中,

JEMALLOC_INLINE arena_t *choose_arena(arena_t *arena){    ......        // xf: 通常情况下线程所绑定的arena记录在arenas_tls中    if ((ret = *arenas_tsd_get()) == NULL) {        // xf: 如果当前thread未绑定arena, 则为其指定一个, 并保存到tls        ret = choose_arena_hard();     }    return (ret);}

初次搜索arenas_tsd_get可能找不到该函数在何处被定义. 实际上, jemalloc使用了一组宏, 来生成一个函数族, 以达到类似函数模板的目的. tsd相关的函数族被定义在tsd.h中.

  1. malloc_tsd_protos - 定义了函数声明, 包括初始化函数boot, get/set函数。
  2. malloc_tsd_externs - 定义变量声明, 包括tls, 初始化标志等等。
  3. malloc_tsd_data - tls变量定义。
  4. malloc_tsd_funcs - 定义了1中声明函数的实现。

与arena tsd相关的函数和变量声明如下,

malloc_tsd_protos(JEMALLOC_ATTR(unused), arenas, arena_t *)malloc_tsd_externs(arenas, arena_t *)malloc_tsd_data(, arenas, arena_t *, NULL)malloc_tsd_funcs(JEMALLOC_ALWAYS_INLINE, arenas, arena_t *, NULL, arenas_cleanup)

当线程还未与任何arena绑定时, 会进一步通过choose_arena_hard寻找一个合适的arena进行绑定. jemalloc会遍历arenas数组, 并按照优先级由高到低的顺序挑选,

  1. 如果找到当前线程绑定数为0的arena, 则优先使用它.
  2. 如果当前已初始化arena中没有线程绑定数为0的, 则优先使用剩余空的数组位置
    构造一个新的arena. 需要说明的是, arenas数组遵循lazy create原则, 初始状态
    整个数组只有0号元素是被初始化的, 其他的slot位置都是null指针. 因此随着新的
    线程不断创造出来, arena数组也被逐渐填满.

  3. 如果1,2两条都不满足, 则选择当前绑定数最小的, 且slot位置更靠前的一个arena.

arena_t * choose_arena_hard(void){    ......    if (narenas_auto > 1) {        ......        first_null = narenas_auto;        // xf: 循环遍历所有arenas, 找到绑定thread数量最小的arena, 并记录        // first_null索引值        for (i = 1; i < narenas_auto; i++) {            if (arenas[i] != NULL) {                if (arenas[i]->nthreads <                    arenas[choose]->nthreads)                    choose = i;            } else if (first_null == narenas_auto) {                first_null = i;            }        }        // xf: 若选定的arena绑定thread为0, 或者当前arena数组中已满, 则返回        // 被选中的arena        if (arenas[choose]->nthreads == 0            || first_null == narenas_auto) {            ret = arenas[choose];        } else {            // xf: 否则构造并初始化一个新的arena            ret = arenas_extend(first_null);        }        ......    } else {        // xf: 若不存在多于一个arena(单核cpu或人为强制设定), 则返回唯一的        // 0号arena        ret = arenas[0];        ......    }    // xf: 将已绑定的arena设置到tsd中    arenas_tsd_set(&ret);    return (ret);}

对比早期的绑定方式, jemalloc的算法显然更加公平, 尽可能的让各个cpu核心平分当前线程,平衡负载.

2.2.4 Arena结构

struct arena_s {    unsigned          ind;            unsigned          nthreads;       malloc_mutex_t    lock;    arena_stats_t       stats;      ql_head(tcache_t)  tcache_ql;    uint64_t           prof_accumbytes;    dss_prec_t             dss_prec;       arena_chunk_tree_t    chunks_dirty;    arena_chunk_t         *spare;    size_t                  nactive;    size_t                  ndirty;    size_t                  npurgatory;    arena_avail_tree_t       runs_avail;    chunk_alloc_t          *chunk_alloc;    chunk_dalloc_t        *chunk_dalloc;    arena_bin_t            bins[NBINS];};
  • ind: 在arenas数组中的索引值.

  • lock: 局部arena lock, 取代传统分配器的global lock. 一般地, 如下操作需要arena lock同步,

    • 线程绑定, 需要修改nthreads.
    • new chunk alloc.
    • new run alloc.
  • stats: 全局统计, 需要打开统计功能.

  • tcache_ql: ring queue, 注册所有绑定线程的tcache, 作为统计用途, 需要打开统计功能.

  • dss_prec: 代表当前chunk alloc时对系统内存的使用策略, 分为几种情况,

typedef enum {            dss_prec_disabled  = 0,            dss_prec_primary   = 1,            dss_prec_secondary = 2,            dss_prec_limit     = 3          } dss_prec_t;  //第一个代表禁止使用系统DSS, 后两种代表是否优先选用DSS. 如果使用primary,  // 则本着先dss->mmap的顺序, 否则按照先mmap->dss. 默认使用dss_prec_secondary.
  • chunks_dirty: rb tree(红黑树), 代表所有包含dirty page的chunk集合. 后面在chunk中会详细介绍.

  • spare: 是一个缓存变量, 记录最近一次被释放的chunk. 当arena收到一个新的chunk alloc请求时, 会优先从spare中开始查找, 由此提高频繁分配释放时, 可能导致内部chunk利用率下降的情况.

  • runs_avail: rb tree, 记录所有未被分配的runs, 用来在分配new run时寻找合适的available run. 一般作为alloc run时的仓库.

  • chunk_alloc/chunk_dalloc: 用户可定制的chunk分配/释放函数, jemalloc提供了默认的版本,

chunk_alloc_default/chunk_dalloc_default
  • bins: bins数组, 记录不同class size可用free regions的分配信息, 后面会详细介绍.

0 0
原创粉丝点击