Windows下C++优化PCIe到NVMe磁盘读写性能的技术咨询
针对你的PCIe数据读取转NVMe写入的应用,以下是具体的软件优化措施:
1. 实现生产者-消费者并行模型,让读取与写入异步执行
当前代码是读一块、写一块的串行逻辑,PCIe读取和磁盘写入互相等待,严重浪费带宽。改用双缓冲区(或多缓冲区)的生产者消费者模型,让PCIe读取数据到空闲缓冲区的同时,磁盘写入另一块缓冲区的数据,实现并行操作:
示例思路:
// 定义两个缓冲区和同步机制 std::array<std::unique_ptr<uint32_t[]>, 2> buffers; std::mutex mtx; std::condition_variable cv; bool buffer_used[2] = {false, false}; size_t buffer_size = dma_block_size_bytes / sizeof(uint32_t); // 生产者线程:负责从PCIe读取数据到空闲缓冲区 void producer(xdma_device& dev) { int idx = 0; while (/* 读取未完成的判断条件 */) { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [&](){ return !buffer_used[idx]; }); // 读取数据到当前缓冲区 size_t bytes_read = dev.read_from_engine(buffers[idx].get(), buffer_size * sizeof(uint32_t)); buffer_used[idx] = true; cv.notify_one(); idx = (idx + 1) % 2; } } // 消费者线程:负责将已填满的缓冲区写入磁盘 void consumer(std::ofstream& outfile) { int idx = 0; while (/* 写入未完成的判断条件 */) { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [&](){ return buffer_used[idx]; }); // 写入缓冲区数据到磁盘 outfile.write(reinterpret_cast<char*>(buffers[idx].get()), buffer_size * sizeof(uint32_t)); buffer_used[idx] = false; cv.notify_one(); idx = (idx + 1) % 2; } }
2. 优化文件写入策略
2.1 增大文件IO缓冲区
默认std::ofstream的缓冲区很小,手动设置更大的缓冲区(比如64KB~2MB),减少系统调用次数:
const size_t file_buf_size = 1024 * 1024; // 1MB std::unique_ptr<char[]> file_buf(new char[file_buf_size]); outfile.rdbuf()->pubsetbuf(file_buf.get(), file_buf_size);
2.2 使用Win32直接IO+异步IO
对于NVMe磁盘,使用**直接IO(FILE_FLAG_NO_BUFFERING)可以绕开系统文件缓存,避免额外的内存拷贝;配合异步IO(FILE_FLAG_OVERLAPPED)**进一步提升并行性:
HANDLE hFile = CreateFile(L"D:\\output.bin", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED | FILE_FLAG_WRITE_THROUGH, NULL); // 后续用WriteFile+OVERLAPPED结构体实现异步写入
注意:使用FILE_FLAG_NO_BUFFERING时,写入的缓冲区大小和偏移必须对齐到磁盘扇区大小(通常是4KB)。
3. 内存对齐与大页分配
PCIe DMA传输对内存地址有对齐要求,使用未对齐的内存会导致额外的拷贝操作。改用对齐内存分配:
// 用VirtualAlloc分配对齐到4KB的内存 size_t aligned_size = (dma_array_elements * sizeof(uint32_t) + 4095) & ~4095; uint32_t* read_data = static_cast<uint32_t*>(VirtualAlloc(NULL, aligned_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)); // 或者用aligned_alloc(C++17+) uint32_t* read_data = static_cast<uint32_t*>(aligned_alloc(4096, dma_array_elements * sizeof(uint32_t)));
同时启用大页内存(需要系统权限),可以减少TLB miss,提升内存访问效率。
4. 调整ReadFile为异步模式
当前read_from_engine使用同步ReadFile,改为异步重叠IO,让PCIe读取操作在后台执行,无需等待完成:
size_t xdma_device::read_from_engine(void* buffer, size_t size, OVERLAPPED& ov) { unsigned long num_bytes_read; if (!ReadFile(c2h0.h, buffer, (DWORD)size, &num_bytes_read, &ov)) { if (GetLastError() != ERROR_IO_PENDING) { throw std::runtime_error("Read failed: " + std::to_string(GetLastError())); } // 后续用GetOverlappedResult等待完成,或绑定回调 } return num_bytes_read; }
5. 优化块大小匹配硬件特性
测试不同的dma_block_size_bytes,匹配PCIe的传输粒度(比如64KB、256KB)和NVMe的最优写入块大小(通常是128KB~1MB)。过小的块会导致频繁的IO调用,过大的块会增加内存占用和延迟。
6. 减少不必要的线程开销
当前代码中on_streaming_read_button_clicked用QtConcurrent::run启动线程,start_read_thread又创建了一个read_thread,属于冗余线程创建。直接在QtConcurrent::run中执行读取逻辑,或者使用Qt的QThreadPool复用线程,减少线程创建销毁的开销。
7. 关闭调试输出
性能测试时关闭qDebug()和std::cout输出,这些IO操作会严重拖慢程序运行速度,仅在调试阶段启用。
内容的提问来源于stack exchange,提问作者Lubron

