LWN: 5.13 中对于内存分配进行的两个优化!

关注了就能看到更多这么棒的文章哦~

A pair of memory-allocation improvements in 5.13

By Jonathan Corbet
May 6, 2021
DeepL assisted translation
https://lwn.net/Articles/855226/

在 5.13 合并的许多改动中,也包括对整个内核的性能提升。这种工作通常不如新功能那么显眼,但它对内核的未来是至关重要的。在内存管理领域,有几个进行了很长时间的 patch set 终于进入了 mainline。它们提供了 bulk page-allocation interface(批量分配页面的接口)以及 vmalloc() 进行 huge-page mapping。这两个改动应该会提升系统的性能,至少是对某些场景来说。

Batch page allocation

内核的内存分配功能长期以来一直在想办法针对性能和可扩展性(scalability)进行优化,但是在有些情况下仍然有改进空间,比如在高速网络(high-speed networking)场景下。早在 2016 年,网络相关的开发人员 Jesper Dangaard Brouer 就介绍了要支持最快的网络链路所面临的挑战:当系统每秒处理数千万个数据包时,用来处理任意一个数据包的时间就非常有限了。内核可能只有几百个 CPU 时钟周期来处理每个数据包,而实际上光是从内存分配器(memory allocator)中分配一个 page 本身的时间开销可能就超过这个限制了。将所有 CPU 时间都花在分配内存上,对获得最佳性能这个目标肯定是不利的。

在当时的文章中,Brouer 要求提供一个 API,可以在一次调用中就分配许多 page,希望能大幅降低每个 page 的分配成本。然后网络处理相关的代码就可以持有一堆内存 page,根据需要来快速分配 page。当时没有人反对这个要求。大家都知道,在这样的情况下,批量操作可以提高吞吐量。但是,这个接口花了不少时间才问世。

Mel Gorman 承担了这项任务,准备了一系列 patch,走到第六个版本的时候,终于在今年 3 月发布并被纳入 -mm 代码树。这组 patch 增加了两个新的接口,用来分配多个单独("order-0")页面,首先介绍这个接口:

unsigned long alloc_pages_bulk(gfp_t gfp, unsigned long nr_pages,
           struct list_head *list);

gfp 用来指定进行分配时的 allocation flag,nr_pages 则是调用者希望分配得到的 page 数量,list 是一个列表用来容纳所分配得到的这些 page。返回值是实际分配出的 page 的数量,毕竟因为一些原因有可能不能满足 nr_pages 的要求。被分配出来的 page structure 用 list 组织起来(利用了 lru entry),然后添加到 list 参数指定的 list 中。

这种用链表来管理返回的各个 page 的做法可能看起来有点奇怪,尤其是通常来说使用链表(linked list)往往不利于得到更好的可扩展性(scalability)。这种做法的优点是它不需要分配内存用来跟踪分配出来的 page。由于这个场景里不太可能会遍历这个 list(因为不会将这个 list 当作一个整体来关注),所以这里并不会引入可扩展性问题。不过,这个接口对一些人来说可能觉得很笨拙,因此对于这些更希望提供一个数组来放置这些指针的人来说,可以使用另一个接口:

unsigned long alloc_pages_bulk_array(gfp_t gfp, unsigned long nr_pages,
           struct page **page_array);

这个函数会把分配出来的 page structure 的指针存储到 page_array 中,当然数组空间至少要有 nr_pages 这么多,否则可能会出问题。有趣的是,这个接口被实现成只会对 page_array 中的 NULL 项才会分配 page 并添加进去,所以 alloc_pages_bulk_array() 可以用来对一个部分清空的 page 数组进行补充填充。因此,在第一次调用 alloc_pages_bulk_array()之前,这个数组必须先被清零。

对于需要进行更多细节控制的用户,实现 alloc_pages_bulk() 和 alloc_pages_bulk_array() 的底层函数实际上是:

unsigned int __alloc_pages_bulk(gfp_t gfp, int preferred_nid,
        nodemask_t *nodemask, int nr_pages,
        struct list_head *page_list,
        struct page **page_array);

这里额外多出来的参数是用来控制从 NUMA 系统中哪个节点来分配 page。会希望优先使用 preferred_nid 这个节点,而 nodemask(如果有指定这个参数的话)则用来指定允许 page 分配来自哪些节点。page_list 和 page_array 中应该有一个不是 NULL,也就会被用来返回分配出的 page。如果两者都提供了的话,那么将会使用 page_array,也就是忽略 page_list。

这组 patch set 所包含的 benchmark 显示,在高速网络场景下的速度提高了近 13%,而 Sun RPC 测试情况下的速度则接近提升了 500%。不过 Gorman 指出:"这个系列的两个可能的使用场景都是一些不太常见的情况(NFS 和 high-speed networks),所以大多数用户短期内可能看不到什么提升。" Sun RPC 和网络场景相关的改动也已经直接合入了 5.13,后面可能会合入一些其他场景下的相关改动。

Huge-page vmalloc()

内核中大多数的内存分配函数都会返回指针,指向 page 或内核的地址空间中的地址。无论是哪种方式,这些指针实际上都对应于被分配出来的内存的物理地址。这对于小范围的内存分配(比如分配一个 page 或更少)场景来说很好,但是由于内存的碎片化,对于更大分配 size 的物理内存分配就变得越来越难以满足。由于这个原因,近年来有许多工作都是为了尽可能地避免进行多个页面的分配(multi-page allocation)。

但有时必须要有一个很大的连续区域才行,这就是 vmalloc()接口使用场景了。vmalloc() 分配出的 page 可能分散在物理内存中多个地方,但是会把它们映射到内核地址空间的一个特殊位置,使得它们看起来是连续的。历史上由于设置 mapping 的成本比较高,以及 32 位系统上这个专用的地址空间比较小,所以不建议过多使用 vmalloc()。不过,现在 64 位系统上不再有地址空间的限制了,所以随着时间的推移对 vmalloc()的使用也在不断增加。

不过,使用 vmalloc() 范围内的地址的话,比起使用内核直接映射(direct mapping)的地址用起来更慢,因为后者会尽可能地使用 huge page 来进行映射。这就减少了 CPU 的 TLB(translation lookaside table)的压力。TLB 就是加速虚拟地址解析(不用经过页表遍历)的。在 vmalloc() 区域内的 mapping 都是使用小 page(所谓的"base" page),这对 TLB 来说压力更大了。

不过从 5.13 开始,vmalloc()可以使用 huge page 来进行一些大范围的内存分配了,这要感谢 Nicholas Piggin 的 patch。对于超过了 huge-page 的最小 size 的空间进行 vmalloc()分配时,将会尝试使用 huge page 而不是 base page。正如 Piggin 所描述的,这可以显著提高某些内核数据结构的性能:

内核中几个最常用的结构(例如 vfs 和 network 的 hash tables)在 NUMA 系统上都是用 vmalloc 分配的,目的是为了在多个机器之间调配访问的吞吐量。对这些结构使用 huge page 来映射就可以大大改善 TLB 的利用,例如,在一个 2-node POWER9 上的 "git diff" 使用中,可以减少近 30 倍的 TLB miss 次数(59,800 -> 2,100),并减少 0.54%的 CPU cycle,因为 vfs hash 是用 2MB 的 page 所分配的了。

这里还是会有一些潜在缺点,比如说由于内部碎片而浪费更多内存。例如,分配 3MB 的内存可能会导致占用两个 2MB 的 huge page,导致有 1MB 内存一直未被使用。当使用 huge page 时,内存在 NUMA 系统中的分布可能不会非常平衡。一些调用 vmalloc() 的函数可对于返回给它的是个 huge page 这个情况没有准备,所以不能所有地方都改过去;特别是 module loader,它就在使用 vmalloc()并且有可能改为 huge page 之后会有好处,但目前并没有改过去。

尽管如此,在目前所做的测试中,为 vmalloc() 使用 huge page 的优点看起来还是超过了缺点。并且设计了一个新增的命令行参数,nohugevmalloc=,用来在需要的情况下禁用这种行为。

这些改动对于大多数用户来说不太可能会提供非常惊人的性能提升。但是它们是正在进行的尽力优化内核行为的工作中的一个重要部分,正是许许多多类似这样的改动使得 Linux 能有如今这么好的表现。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值