文章

总结:内存访问优化

process_stack

1. 虚拟内存分配

1.1 mmap

mmap用于建立文件映射,或者匿名映射

当用于文件映射时,mmap将文件内容缓存进内核空间的page cache里面,然后将用户的一段虚拟内存空间直接映射到page cache。用户通过访问这段虚拟内存,直接读写内核空间上的page cache,避免buffer拷贝开销及用户态的切换。

mmap用户建立匿名映射时,将用户空间的一段虚拟内存空间直接映射到某段物理内存上,这段虚拟内存称为匿名页匿名映射用于malloc操作(大于128KB)。

mmap文件知识点:

  • 通常情况下(除了MAP_POPULATE),mmap创建时,只是在用户空间分配一段地址空间(VMA),只有访问地址空间时,才会分配物理地址空间(Page fault中断分配内存),并更新映射到VMA,建立映射关系。
  • mmap映射的物理内存,可以跨进程共享,但需要进程之间加锁访问(写操作)。如果多个进程写同一个mmap映射的物理内存,会触发Copy On Write(COW),内核重新分配一个新的物理内存,并复制原有物理内存的内容。
  • 通过msync()将内存写回硬盘,munmap()释放内存。

MAP_POPULATE标志位: 建立页表,这将使得内核进行一些预读(实测没有性能提升)。使用方式:

  • 使用open + 选项O_RDONLY | O_DIRECT打开文件;
  • 以及使用mmap + MAP_POPULATE选项,在打开文件时建立页表。

MAP_LOCKED标志位: 锁定映射内存,阻止被换出。类似于mlock()

更多I/O相关: about IO performance

1.2 malloc / free

在现代操作系统中,malloc的作用是分配虚拟内存空间,并不实际分配物理内存。当分配的虚拟内存空间第一次被访问时,才会真正的分配物理内存(OS的写时分配行为)。

malloc行为:

  1. 首先尝试在进程空间内存池中查找有没有可以重用的内存空间,如果没有才会进行系统调用。
  2. 如果申请的内存小于128KB,会通过调用brk()函数申请内存:根据申请内存大小,将堆顶指针向上移动,并返回新申请的内存地址给用户;当freebrk()分配的内存时,并不会将物理内存缓存归还给操作系统,而是放到malloc的内存池中,待下次再次申请时直接使用。
  3. 申请的内存大于128KB,会通过调用mmap()函数申请内存:通过匿名映射获取虚拟内存;当freemmap()分配的内存时,会将物理内存缓存归还给操作系统。

malloc-brk malloc-mmap

1.3 new / delete

new / delete操作时,在调用malloc/free基础上,对non-trival对象,调用其构造/析构函数;对于trival对象,不需要调用构造/析构函数,直接分配/释放内存。

2. 用户态 malloc

  • 用户态内存分配:intel TBB malloc, tcmalloc,Vulkan Memory Allocator等。( 经测试,microsoft mimalloc适配性不是很好,使用过程中会出错;intel TBB malloc overhead似乎比较大)
  • 内存池。(见下面资料链接)
  • 对象池。

2.1 参考资料

2.2 内存池仓库

3. Linux Huge page

设置Huge Page,减少内存访问需要的的缺页中断,提高内存访问效率。以及减少TLB未命中导致的性能下降 – 未命中TLB则需要逐级查询page table

Linux使用Huge Page有两种方式:

3.1 Transparent Huge Page

主流Linux kernel发布版本都是默认支持THP的(TRANSPARENT_HUGEPAGE)。但是在用户态使能,需要开启设置khugepaged: kernel – Transparent Hugepage Support

1
2
3
$ cat /sys/kernel/mm/transparent_hugepage/enabled

always [madvise] never

设置khugepaged:

1
2
# run as root
echo "always" >! /sys/kernel/mm/transparent_hugepage/enabled

选项:

  • always: 开启THP,内核尝试将连续的内存页合并成一个THP页。用户层不需要任何操作。
  • madvise: 开启THP,内核尝试将连续的内存页合并成一个THP页。用户层可以使用madvise()系统调用,将内存标记为THP

x86-64系统,THP页大小为2MB。查看Huge Page的页大小:

1
cat /sys/kernel/mm/transparent_hugepage/hpage_pmd_size

使用THP,需要在分配内存时,需要将分配的内存对齐,比如2MB大页对齐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <sys/mman.h>
 
// ... definition of is_huge() and is_thp() ...
 
constexpr size_t HPAGE_SIZE = 2 * 1024 * 1024;
 
int main() {
  auto size = 4 * HPAGE_SIZE;
  void *mem = aligned_alloc(HPAGE_SIZE, size);

  madvise(mem, size, MADV_HUGEPAGE);

  // Make sure the page is present
  static_cast<char *>(mem)[0] = 'x';

  std::cout << "Is huge? " << is_huge(mem) << "\n";
  std::cout << "Is THP? " << is_thp(mem) << "\n";
}

完整代码thp.cpp

1
g++ --std=c++17 thp.cpp -o thp

3.2 HugeTLB

可以通过仅仅链接libhugetlbfs,即可使用大页内存:

1
2
3
4
5
6
7
8
9
10
# https://github.com/libhugetlbfs/libhugetlbfs

sudo apt-get install libhugetlbfs-dev libhugetlbfs-bin -y
sudo ln -s /usr/bin/ld.hugetlbfs /usr/share/libhugetlbfs/ld

# 分配1000个大页,一个page大小为2MB,总共2GB. 需要管理员权限
echo 1000 > /proc/sys/vm/nr_hugepages

# 确认大页是否可用
cat /proc/meminfo | grep HugePages_

链接libhugetlbfs:

1
-B /usr/share/libhugetlbfs -Wl,--hugetlbfs-align -no-pie -Wl,--no-as-needed

参考 ARM – Introduction to libhugetlbfs

另外,使用HugeTLB分配一个10MB匿名映射:

1
2
void *addr = mmap(0, 10*1024*1024, (PROT_READ | PROT_WRITE),
                  (MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB), 0, 0);

参考:

本文由作者按照 CC BY 4.0 进行授权