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;
int fd = open(filepath, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); if (fd == -1) { std::cerr << "Failed to open file\n"; return 1; }
if (ftruncate(fd, filesize) == -1) { std::cerr << "Failed to set file size\n"; close(fd); return 1; }
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);
char* data = static_cast<char*>(mapped); std::strcpy(data, "Hello, memory mapped file!");
msync(mapped, filesize, MS_SYNC);
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;
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; }
HANDLE hMapping = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, filesize, NULL); if (!hMapping) { std::cerr << "CreateFileMapping failed\n"; CloseHandle(hFile); return 1; }
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);
char* data = static_cast<char*>(mapped); strcpy_s(data, filesize, "Hello from Windows!");
FlushViewOfFile(mapped, filesize);
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);
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和进程间通信的强大工具,合理使用能显著提升程序效率。