如何在C++中预先提交分配的内存

How to eager commit allocated memory in C++?

本文关键字:提交 分配 内存 C++      更新时间:2023-10-16

一般情况

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

这是一个强大的工作站,具有3个Pascal GPU,高端Haswell处理器和四通道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 16 倍带宽。如果可以的话,我已经需要进一步推动了。

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

自旋锁定性能下降似乎非常罕见(因为用例将其推得如此之远),以至于在Google上,您找不到旋转锁定函数名称的单个结果。

升级到可以更好地控制映射(Vulkan)的API(Vulkan)正在进行中,但它不适合作为短期修复。出于同样的原因,切换到更好的操作系统内核目前不是一种选择。

减少 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 时避免任何形式的并发页面错误。