工具内存优化(C++)

1. 背景

优化工具程序的peak memory,使其在产线能稳定运行较长时间,尤其是多线程场景,不需要在某个时间点产线暂停,然后重启程序。

2. 优化手段

  • 避免创建大结构体
  • 减少深拷贝,多使用引用
  • 用到的时候再分配内存
  • 及时释放内存
  • 以时间换空间 (将一个大的节点,在代码层面拆成多个小节点进行顺序执行,降低memory peak,但运行时间增加)
  • 选用合适的输入文件格式,减少解析占有的内存空间(例如将xml换成json或者压缩后的二进制文件)

3. 实例

如果程序在运行过程中需要不断的保存生成的较多数据,最后再统一把他们从内存中拿出,以某种文件格式存到硬盘空间。 当前程序这种运作机制的话,我们是有优化的空间,可以在产生数据的过程中设置一个阈值,达到多少MB,就将其从内存中push到磁盘空间。 在Windows系统中,我们可以进行内存映射文件操作来实现,通常需要直接调用操作系统的底层API,因为C++标准库目前(截至C++23)还没有提供统一的内存映射文件接口。

3.1. Memory-Map File

内存映射文件不是一种具体的文件格式,而是操作系统提供的一种高效文件读写机制。它能把磁盘上的整个文件或部分区域直接映射到应用程序的内存地址空间中。 这意味着在程序里,可以像操作普通内存数组一样(通过指针)来访问文件内容,完全不用手动调用read()或read()接口。

3.2. 核心原理:为什么它很快?

  • 按需加载:映射时操作系统并不会真的把文件全部读入内存。只有当代码访问到某个地址时,才会触发“缺页中断”去加载相应的数据块,因此能处理远大于物理内存的文件。
  • 零拷贝访问:传统读写需要在内核和应用程序之间复制两次数据,而mmp直接在应用程序的虚拟地址空间操作,实现了“零拷贝”,大幅提升了性能。
  • 自动会写:修改内存中的数据,操作系统会在合适的时机自动同步到磁盘(或可手动调用msync/flush)。

3.3. 如何操作内存映射文件?

在不同环境下,核心操作都是“创建映射->访问数据->解除映射”。

C/C++通过系统调用直接操作,用于高性能系统编程。

3.4. 跨平台API概览

平台 打开文件 创建映射对象 映射视图
POSIX open() mmap() 直接使用mmap返回指针
Windows CreateFile() CreateFileMapping() MapViewOfFile()

3.5. POSIX(Linux/MacOS)示例

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#include <cstring>
#include <iostream>

int main() {
const char* filepath = "example.dat";
size_t filesize = 4096; // 假设文件大小

// 1. 打开文件
int fd = open(filepath, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
std::cerr << "Failed to open file\n";
return 1;
}

// 2. 扩展文件大小(如果映射写入,文件必须具有实际大小)
if (ftruncate(fd, filesize) == -1) {
std::cerr << "Failed to set file size\n";
close(fd);
return 1;
}

// 3. 内存映射
void* mapped = mmap(nullptr, filesize, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (mapped == MAP_FAILED) {
std::cerr << "mmap failed\n";
close(fd);
return 1;
}
close(fd); // 映射后可以关闭文件描述符

// 4. 使用映射内存
char* data = static_cast<char*>(mapped);
std::strcpy(data, "Hello, memory mapped file!");

// 5. 强制同步到磁盘(可选)
msync(mapped, filesize, MS_SYNC);

// 6. 解除映射
munmap(mapped, filesize);

return 0;
}

3.6. 现代C++ RAII封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#include <stdexcept>
#include <string>

class MemoryMappedFile {
public:
MemoryMappedFile(const std::string& path, size_t size)
: data_(nullptr), size_(size) {
fd_ = open(path.c_str(), O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd_ == -1)
throw std::runtime_error("Failed to open file");

if (ftruncate(fd_, size_) == -1) {
close(fd_);
throw std::runtime_error("Failed to set file size");
}

data_ = mmap(nullptr, size_, PROT_READ | PROT_WRITE,
MAP_SHARED, fd_, 0);
if (data_ == MAP_FAILED) {
close(fd_);
throw std::runtime_error("mmap failed");
}
close(fd_); // 可关闭
}

~MemoryMappedFile() {
if (data_ && data_ != MAP_FAILED) {
munmap(data_, size_);
}
}

// 禁止拷贝
MemoryMappedFile(const MemoryMappedFile&) = delete;
MemoryMappedFile& operator=(const MemoryMappedFile&) = delete;

// 允许移动
MemoryMappedFile(MemoryMappedFile&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
}

char* data() { return static_cast<char*>(data_); }
const char* data() const { return static_cast<const char*>(data_); }
size_t size() const { return size_; }

void sync() {
msync(data_, size_, MS_SYNC);
}

private:
void* data_;
size_t size_;
int fd_; // 仅用于构造,之后可关闭
};

3.7. Windows示例

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <windows.h>
#include <iostream>

int main() {
const char* filepath = "example.dat";
DWORD filesize = 4096;

// 1. 打开文件
HANDLE hFile = CreateFileA(filepath,
GENERIC_READ | GENERIC_WRITE,
0, // 不共享
NULL,
OPEN_ALWAYS, // 存在则打开,否则创建
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile == INVALID_HANDLE_VALUE) {
std::cerr << "CreateFile failed\n";
return 1;
}

// 2. 创建文件映射对象
HANDLE hMapping = CreateFileMappingA(hFile,
NULL,
PAGE_READWRITE, // 保护属性
0, // 映射对象大小高32位
filesize, // 低32位
NULL); // 名称(可用于进程间共享)
if (!hMapping) {
std::cerr << "CreateFileMapping failed\n";
CloseHandle(hFile);
return 1;
}

// 3. 映射视图到进程地址空间
void* mapped = MapViewOfFile(hMapping,
FILE_MAP_ALL_ACCESS, // 访问模式
0, 0, // 偏移量
filesize); // 映射大小
if (!mapped) {
std::cerr << "MapViewOfFile failed\n";
CloseHandle(hMapping);
CloseHandle(hFile);
return 1;
}

// 可以关闭句柄,视图仍然有效
CloseHandle(hMapping);
CloseHandle(hFile);

// 4. 使用映射内存
char* data = static_cast<char*>(mapped);
strcpy_s(data, filesize, "Hello from Windows!");

// 5. 刷新到磁盘(可选)
FlushViewOfFile(mapped, filesize);

// 6. 解除映射
UnmapViewOfFile(mapped);

return 0;
}

3.8. 使用Boost.Interprocess

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <boost/interprocess/file_mapping.hpp>
#include <boost/interprocess/mapped_region.hpp>
#include <iostream>

namespace bip = boost::interprocess;

int main() {
// 打开或创建文件
std::filebuf fbuf;
fbuf.open("test.bin", std::ios_base::in | std::ios_base::out |
std::ios_base::trunc | std::ios_base::binary);
fbuf.pubseekoff(4095, std::ios_base::beg);
fbuf.sputc(0); // 扩展文件到 4096 字节

// 创建文件映射
bip::file_mapping fmapping("test.bin", bip::read_write);
bip::mapped_region region(fmapping, bip::read_write, 0, 4096);

// 获取地址
void* addr = region.get_address();
std::memset(addr, 0, region.get_size());

// 强制同步
region.flush();

return 0;
}

4. 总结

在C++中使用内存映射文件需要直接与操作系统API交互,但可以通过良好的封装可以简化使用。对于跨平台项目,推荐使用Boost.Interprocess或自己编写条件编译的RAII封装。内存映射是处理大文件、实现高性能I/O和进程间通信的强大工具,合理使用能显著提升程序效率。