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


