c++ - 如何在 C++ 中急切提交分配的内存?

标签 c++ windows multithreading memory directx-11

一般情况

在带宽、CPU 使用率和 GPU 使用率方面都非常密集的应用程序需要每秒从一个 GPU 传输大约 10-15GB 到另一个 GPU。它使用 DX11 API 来访问 GPU,因此上传到 GPU 只能发生在每次上传都需要映射的缓冲区中。上传一次以 25MB 的 block 进行,16 个线程同时将缓冲区写入映射缓冲区。对于这一切,没有什么可以做的。如果不是因为下面的 bug,写的实际并发级别应该更低。

这是一款功能强大的工作站,配备 3 个 Pascal GPU、高端 Haswell 处理器和四 channel RAM。硬件上没有太多可以改进的地方。它运行的是 Windows 10 桌面版。

实际问题

一旦我通过了约 50% 的 CPU 负载,MmPageFault() 中的某些内容(在 Windows 内核中,在访问已映射到您的地址空间但操作系统未提交的内存时调用然而)严重中断,剩余的 50% CPU 负载被浪费在 MmPageFault() 内的自旋锁上。 CPU 利用率达到 100%,应用程序性能完全下降。

我必须假设这是由于每秒需要分配给进程的大量内存,并且每次取消映射 DX11 缓冲区时也完全从进程中取消映射。相应地,它实际上是每秒对 MmPageFault() 的数千次调用,随着 memcpy() 顺序写入缓冲区而顺序发生。对于遇到的每个未提交的页面。

当 CPU 负载超过 50% 时,Windows 内核中保护页面管理的乐观自旋锁会完全降低性能。

注意事项

缓冲区由 DX11 驱动程序分配。分配策略没有什么可以调整的。无法使用不同的内存 API,尤其是无法重复使用。

对 DX11 API 的调用(映射/取消映射缓冲区)都发生在单个线程中。实际的复制操作可能发生在多线程上,所涉及的线程多于系统中的虚拟处理器数量。

降低内存带宽要求是不可能的。这是一个实时应用程序。事实上,目前的硬限制是主 GPU 的 PCIe 3.0 16x 带宽。如果可以的话,我已经需要更进一步了。

避免多线程拷贝是不可能的,因为存在无法简单合并的独立生产者-消费者队列。

自旋锁性能下降似乎非常罕见(因为用例将它推到那么远),以至于在 Google 上,您找不到任何与自旋锁函数名称相关的结果。

正在升级到对映射 (Vulkan) 提供更多控制的 API,但它不适合作为短期修复。出于同样的原因,目前不能切换到更好的操作系统内核。

降低 CPU 负载也不起作用;除了(通常是微不足道且成本低廉的)缓冲区拷贝之外,还有太多工作要做。

问题

可以做什么?

我需要显着减少单个页面错误的数量。我知道已经映射到我的进程中的缓冲区的地址和大小,我也知道内存还没有被提交。

如何确保以尽可能少的事务提交内存?

DX11 的特殊标志可以防止在取消映射后取消分配缓冲区,Windows API 强制在单个事务中提交,几乎任何东西都是受欢迎的。

当前状态

// In the processing threads
{
    DX11DeferredContext->Map(..., &buffer)
    std::memcpy(buffer, source, size);
    DX11DeferredContext->Unmap(...);
}

最佳答案

当前的解决方法,简化的伪代码:

// During startup
{
    SetProcessWorkingSetSize(GetCurrentProcess(), 2*1024*1024*1024, -1);
}
// In the DX11 render loop thread
{
    DX11context->Map(..., &resource)
    VirtualLock(resource.pData, resource.size);
    notify();
    wait();
    DX11context->Unmap(...);
}
// In the processing threads
{
    wait();
    std::memcpy(buffer, source, size);
    signal();
}

VirtualLock()强制内核立即使用 RAM 支持指定的地址范围。对补充 VirtualUnlock() 的调用函数是可选的,本地址范围从进程中取消映射时,它会隐式发生(并且没有额外的成本)。 (如果显式调用,它的成本约为锁定成本的 1/3。)

为了让 VirtualLock() 能够正常工作,SetProcessWorkingSetSize()需要首先调用,因为由 VirtualLock() 锁定的所有内存区域的总和不能超过为进程配置的最小工作集大小。将“最小”工作集大小设置为高于进程的基线内存占用量没有副作用,除非您的系统实际上可能进行交换,您的进程仍然不会消耗比实际工作集大小更多的 RAM。


仅使用 VirtualLock(),尽管在​​单个线程中使用延迟 DX11 上下文进行 Map/Unmap 调用,但确实立即减少了性能损失从 40-50% 到稍微可接受的 15%。

放弃使用延迟上下文,触发所有软故障,以及在单个线程上取消映射时相应的取消分配,给出了必要的性能提升。该自旋锁的总成本现在已降至总 CPU 使用率的 <1%。


总结?

当您预计 Windows 上会出现软故障时,请尽量将它们全部保留在同一个线程中。执行并行 memcpy 本身是没有问题的,在某些情况下甚至需要充分利用内存带宽。但是,这仅在内存已经提交到 RAM 的情况下。 VirtualLock() 是确保这一点的最有效方法。

(除非您使用像 DirectX 这样将内存映射到进程的 API,否则您不太可能经常遇到未提交的内存。如果您只是使用标准 C++ newmalloc 无论如何,你的内存都会在你的进程中被汇集和回收,所以软故障很少见。)

确保在使用 Windows 时避免任何形式的并发页面错误。

关于c++ - 如何在 C++ 中急切提交分配的内存?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45242210/

相关文章:

multithreading - 防止 Delphi MDI 应用程序在外部 DLL 中创建 TApplication

c++ - "data ready"标志的同步机制?

c++ - 具有相同模板类型的模板类中的模板方法

c++ - 为什么我的 C++ 代码在使用 read(...) 函数后会导致段错误?

windows - DLL 从其父级(加载程序)获取符号

python - 为什么Python的pathlib和os lib对于Windows中的映射网络驱动器返回不同的结果?

c++ - 为队列中的对象同时执行函数的奇怪行为

c++ - ZMQ Hello world 不起作用

c++ - 是否可以在 C++ 的控制台中放大文本

javascript - 如何使用javascript或nodejs在windows/mac中查找软件是否安装