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

例说局部性原理给程序带来的提升

网上介绍了很多局部性原理的好处,本文结合笔者最近的遭遇,简单的做个分享。
局部性原理就不介绍了,下面直接上例子。
我们以linux内核v6.15的函数collect_longterm_unpinnable_folios为例,这里简单的假设没有启用HVO特性。

static inline const struct page *page_fixed_fake_head(const struct page *page)
{return page;
}static __always_inline unsigned long _compound_head(const struct page *page)
{unsigned long head = READ_ONCE(page->compound_head);if (unlikely(head & 1))return head - 1;return (unsigned long)page_fixed_fake_head(page);
}#define page_folio(p)		(_Generic((p),				\const struct page *:	(const struct folio *)_compound_head(p), \struct page *:		(struct folio *)_compound_head(p)))static struct folio *pofs_get_folio(struct pages_or_folios *pofs, long i)
{if (pofs->has_folios)return pofs->folios[i];return page_folio(pofs->pages[i]);
}static unsigned long collect_longterm_unpinnable_folios(struct list_head *movable_folio_list,struct pages_or_folios *pofs)
{unsigned long i, collected = 0;struct folio *prev_folio = NULL;bool drain_allow = true;for (i = 0; i < pofs->nr_entries; i++) {struct folio *folio = pofs_get_folio(pofs, i);if (folio == prev_folio)continue;prev_folio = folio;*********
}

简单解释一下函数的逻辑,pofs是一个数组,数组长pofs->nr_entries,这个数组是由gup接口,通过页表,按PAGE_SIZE的步长,获取到的由用户传入的一个虚拟地址范围里对应的page/folio,函数collect_longterm_unpinnable_folios的目的就是筛选出这个数组里的page/folio是否属于“longterm_unpinnable”。逻辑非常简单,遍历数组,然后检查。
这个函数的逻辑没有什么问题,但现如今程序为了提升性能,会尽可能的使用大页(THP或者HUGETLBFS),当gup接口碰到这种情况的时候,pofs里相邻的page其实就是属于同一个large folio,这种情况下,就为局部性原理带来了可施展的空间。
那具体怎么施展的呢?
先放上优化后的代码,再做分析。

@@ -2296,6 +2296,31 @@ static void pofs_unpin(struct pages_or_funpin_user_pages(pofs->pages, pofs->nr_entries);}+static struct folio *pofs_next_folio(struct folio *folio,
+                struct pages_or_folios *pofs, long *index_ptr)
+{
+        long i = *index_ptr + 1;
+
+        if (!pofs->has_folios && folio_test_large(folio)) {
+                const unsigned long start_pfn = folio_pfn(folio);
+                const unsigned long end_pfn = start_pfn + folio_nr_pages(folio);
+
+                for (; i < pofs->nr_entries; i++) {
+                        unsigned long pfn = page_to_pfn(pofs->pages[i]);
+
+                        /* Is this page part of this folio? */
+                        if (pfn < start_pfn || pfn >= end_pfn)
+                                break;
+                }
+        }
+
+        if (unlikely(i == pofs->nr_entries))
+                return NULL;
+        *index_ptr = i;
+
+        return pofs_get_folio(pofs, i);
+}
+/** Returns the number of collected folios. Return value is always >= 0.*/
@@ -2303,16 +2328,12 @@ static void collect_longterm_unpinnable_struct list_head *movable_folio_list,struct pages_or_folios *pofs){
-        struct folio *prev_folio = NULL;bool drain_allow = true;
-        unsigned long i;
+        struct folio *folio;
+        long i = 0;-        for (i = 0; i < pofs->nr_entries; i++) {
-                struct folio *folio = pofs_get_folio(pofs, i);
-
-                if (folio == prev_folio)
-                        continue;
-                prev_folio = folio;
+        for (folio = pofs_get_folio(pofs, i); folio;
+                 folio = pofs_next_folio(folio, pofs, &i)) {if (folio_is_longterm_pinnable(folio))continue;

我们看原始collect_longterm_unpinnable_folios()函数循环体里面,每次循环获取一个folio,然后与prev_folio做比较,相同则跳过。
从pofs_get_folio()函数的实现就可以发现,如果我们将这个比较放进pofs_get_folio()函数函数中,那就可以省去不少if (pofs->has_folios)的判断了,由此带来的第一个优化点,就是branch数的减少,从而能够提升cache/tlb的命中率。
继续深入探究,page_folio()的实现,本质是调用了_compound_head(),这个函数的第一行就是一个READ_ONCE(),根据文章https://quant67.com/post/linux/access_once.html 我们知道,这是一个阻碍编译器做优化的函数,并且使用了READ_ONCE()之后,数据必须从内存/cache中去获取,而不能将其暂存在寄存器里,笔者简单试了一下,将这里的READ_ONCE去掉,gup接口能有不足5%的提升(可能只有1%,性能测试结果一直在波动,但是都是正向的。想想一行代码就影响了一个那么复杂的函数的性能就可怕),当然这里的READ_ONCE()肯定不能删掉,而是尝试去减少对他的调用,这是第二个优化点。
那怎么优化掉呢?本质上,也就是需要识别,下一个page,是否和当前page属于同一个large folio,那这简单,我们可以通过pfn就可以实现,large folio是一个大页,其物理地址肯定是连续的,那我们就可以看看下一个page是否属于这个物理地址范围就可以了。
从而接下来的优化就水到渠成了,我们把if (folio == prev_folio)给“搬进”函数pofs_get_folio(),只需要不断的获取下一个page的pfn,并进行判断即可。如果下一个page的pfn属于当前large folio的物理地址范围,那显然if (folio == prev_folio)成立,可以继续检查后一个page,否则,不属于这个large folio,需要做是否是longterm_unpinnable的检查。可以看到,如果我们遇到了连续的page属于同一个large folio,我们的代码就是读取数组下一个成员,并做一个简单的if判断,接着下一个成员做判断,这样不断地循环,函数的主要流程就能收缩在这两个连续的指令,这就是典型的使用局部性原理来优化程序,这是优化点三
那么,原先代码里的if (folio == prev_folio),就不属于局部性原理的优化了吗?当然属于,prev_folio是上一次访问的folio,如果运气好,肯定还在cache里,就可以很迅速地访问到。但是,鉴于pofs_get_folio()实现的较为复杂,有好几层调用,这样编译器可能就无法充分发挥其优化,并且,对于cache/tlb buffer比较小的cpu,可能因为较长的函数调用链,就会把本来想利用的局部性原理的数据/cache给冲掉了,从而对性能有较大的折扣。而这个优化的方案,相当于是把局部性原理做的更极致了而已。

http://www.xdnf.cn/news/12908.html

相关文章:

  • 2480: 2020年06月2级T1:计算矩阵边缘元素之和
  • 计数思想-众数
  • vmware 设置 dns
  • 存储的基本原理
  • 哈希map中不能将数组作为键的原因 leetcode49
  • 第二十八章 字符串与数字
  • 5G-A通感融合对监控监督体系的核心作用
  • 下一代设备健康管理解决方案:基于多源异构数据融合的智能运维架构
  • AD规则设置-铜皮规则,阻焊规则,实时DRC
  • 栈和队列的奇妙冒险:用栈实现队列
  • 6个月Python学习计划 Day 17 - 继承、多态与魔术方法
  • 快速上手Linux文本流编辑器sed
  • 智慧城市项目总体建设方案(Word700页+)
  • 基于深度强化学习的智能机器人导航系统
  • 黑马Javaweb Request和Response
  • 05.查询表
  • 【无人机】地面站crazyfile-cfclient免安装方法,Python3.10的整体环境配置打包
  • OCS2库及其在足式机器人上的应用
  • RK3568项目(七)--uboot系统之外设与PMIC详解
  • 真实案例分享,Augment Code和Cursor那个比较好用?
  • PDF 转 Word 工具 拖拽秒转可编辑文档,批量处理保留原格式
  • 用通俗的话解释下MCP是个啥?
  • android 模拟器如何进行单模块更新
  • 【设计模式】1.简单工厂、工厂、抽象工厂模式
  • ORACLE 修改端口号之后无法启动?
  • 港理工:LLM推理与推荐能力集成
  • ElGamal加密算法:离散对数难题的安全基石
  • (五)Linux性能优化-CPU-性能优化
  • GitOps 核心思想 - 当 Git 成为唯一信源
  • 2025-04-22-X86 架构与 Arm 架构异同及应用