#pages #memory #up #binary #segment #virtual-memory #priming

page-primer

通过“预取”二进制中的内存页来加速你的程序

2个不稳定版本

新版本 0.2.0 2024年8月21日
0.1.0 2024年8月21日

#168 in Unix APIs

Download history 110/week @ 2024-08-15

123 个月下载量

MIT/Apache

32KB
506 行代码(不包括注释)

page-primer

page-primer通过“预取”你的二进制中的内存页来加速你的Rust程序执行。它支持两种优化:

  • mlock()ing ELF segments so they are never paged out, avoiding "major page fault" stalls while waiting for them to be read back in from SSD or even spinning disk.
  • remapping ELF segments to enable huge pages, which can speed up large programs by 5–10%. (More below.)

page-primer目前只在Linux上工作,尽管至少mlock()的概念应该适用于任何基于Unix的系统。

何时使用它?

  • 应用程序中,而不是
  • 运行在Linux上的
  • 并且是长期运行
  • 如果你关心CPU效率和/或延迟
  • 并且你能够承担一些额外RAM
  • 并且你可以接受一些unsafe

如何使用它?

  1. main()的顶部附近,在创建任何线程之前,添加以下代码
    let prime_out = page_primer::prime()
        .mlock(true)
        .remap(true) // if desired, see notes.
        .run();
    
  2. main()中更低的位置,在设置日志提供者之后,添加以下代码
    prime_out.log();
    
  3. 如果使用重映射,请将以下内容添加到你的.cargo/config.toml
    [target.x86_64-unknown-linux-gnu]
    rustflags = [
        "-C", "link-arg=-z",
        "-C", "link-arg=common-page-size=2097152",
        "-C", "link-arg=-z",
        "-C", "link-arg=max-page-size=2097152",
    ]
    
  4. 验证性能改进!

一个注意事项是,如果你后来dlopen了一些动态库,这段代码将不知道如何预取它。

重映射和巨大页

虚拟内存背景:页面、巨大页面和转页巨大页面

现代操作系统/处理器使用虚拟内存:用户空间进程看到的内存地址并不直接代表物理内存位置。相反,虚拟地址空间被划分为“页面”,其含义由操作系统维护的“页面表”定义。CPU的内存映射单元(MMU)通过查询页面表将虚拟内存地址转换为物理内存地址。这种映射有助于隔离进程,以实现安全性和可靠性等好处。

随着系统RAM容量的增长到千兆甚至更高,页面大小一般没有改变:在Linux/x86_64上仍然是4 KiB。这是一个问题!页面表变得太大,CPU无法快速查询。尽管CPU在转换后备缓冲器(TLB)中缓存页面表,但它们通常花费15%的时间在TLB缓存未命中上停滞。

解决方案是更大的页面。Linux/x86_64默认仍然使用4 KiB页面,但也支持2 MiB或1 GiB的大页面。这代表相同TLB空间的256或262,144倍RAM。当它们被广泛使用时,CPU在TLB未命中上等待的时间将大大减少。

Linux现在甚至有透明大页面(THP),它会被自动使用。但只是在某些情况下。它不会在“真实”文件系统(例如ext4或btrfs)上使用透明大页面,除非你的内核编译时启用了实验选项CONFIG_READ_ONLY_THP_FOR_FS=y。大多数发行版的内核都没有这样做。这意味着你的程序的可执行文件将无法利用大页面。除非...

大页面的重映射

程序可以启动,创建一个新的大页面合格的内存映射,将它们的代码复制到它上面,并重映射到现有代码的位置。这个想法比听起来更合理,并且已经存在了一段时间。

  • libhugetlbfs自2005年以来就支持这个想法。但它使用hugetlbfs,它很挑剔。系统管理员必须安排在启动时预留页面,并挂载一个特殊的文件系统。
  • Google通过匿名mmap重映射,在它的数据中心运行的大多数服务器上,以及在ChromeOS上。
  • Facebook通过匿名mmapHHVM中重映射。

page-primer的实现使用memfd_create。在此之前,/proc/<pid>/maps可能看起来像这样

5646c542d000-5646c55ef000 r--p 00000000 103:03 69612122                  /home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr
5646c562d000-5646c66f3000 r-xp 00200000 103:03 69612122                  /home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr
5646c682d000-5646c6d74000 r--p 01400000 103:03 69612122                  /home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr
5646c7143000-5646c722d000 r--p 01b16000 103:03 69612122                  /home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr
5646c722d000-5646c7230000 rw-p 01c00000 103:03 69612122                  /home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr
...

之后,它将看起来像这样

5646c5400000-5646c542d000 r--p 00000000 00:01 25220866                   /memfd:/home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr (deleted)
5646c542d000-5646c55ef000 r--p 0002d000 00:01 25220866                   /memfd:/home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr (deleted)
5646c55ef000-5646c5600000 r--p 001ef000 00:01 25220866                   /memfd:/home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr (deleted)
5646c5600000-5646c562d000 r-xp 00000000 00:01 25220867                   /memfd:/home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr (deleted)
5646c562d000-5646c66f3000 r-xp 0002d000 00:01 25220867                   /memfd:/home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr (deleted)
5646c66f3000-5646c6800000 r-xp 010f3000 00:01 25220867                   /memfd:/home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr (deleted)
5646c6800000-5646c682d000 r--p 00000000 00:01 25220868                   /memfd:/home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr (deleted)
5646c682d000-5646c6d74000 r--p 0002d000 00:01 25220868                   /memfd:/home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr (deleted)
5646c6d74000-5646c6e00000 r--p 00574000 00:01 25220868                   /memfd:/home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr (deleted)
5646c7000000-5646c7143000 rw-p 00000000 00:01 25220869                   /memfd:/home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr (deleted)
5646c7143000-5646c7231000 rw-p 00143000 00:01 25220869                   /memfd:/home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr (deleted)
5646c7231000-5646c7400000 rw-p 00231000 00:01 25220869                   /memfd:/home/slamb/git/moonfire-nvr/server/target/debug/moonfire-nvr (deleted)

有一个显著的缺点:这种重映射可能会破坏一些调试和性能分析工具获取回溯的能力。如果您知道可以解决这个问题的方法,请告诉我!

故障排除

如果您的程序的LOAD部分与您的平台透明大页面大小的倍数对齐,则重映射效果最佳。上面的.cargo/config.toml片段应该可以完成这项工作。您可以通过readelf来验证它是否成功。

$ readelf --segments target/debug/examples/simple
...
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
...
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000035308 0x0000000000035308  R      0x200000
  LOAD           0x0000000000200000 0x0000000000200000 0x0000000000200000
                 0x000000000020d391 0x000000000020d391  R E    0x200000
  LOAD           0x0000000000600000 0x0000000000600000 0x0000000000600000
                 0x0000000000079468 0x0000000000079468  R      0x200000
  LOAD           0x00000000007e5d60 0x00000000009e5d60 0x00000000009e5d60
                 0x000000000001a338 0x000000000001a580  RW     0x200000
...

最后,内核实际上应该以这些边界的倍数对齐的方式加载代码,这可以通过/proc/<pid>/maps来验证。这应该在Linux内核5.10及更高版本中发生。更精确地说,您的内核应该有提交ce81bb256a224259ab686742a6284930cbe4f1fa

依赖关系

~140KB