如何在 C++ 中急切提交分配的内存?
Posted
技术标签:
【中文标题】如何在 C++ 中急切提交分配的内存?【英文标题】:How to eager commit allocated memory in C++? 【发布时间】:2017-12-27 18:34:39 【问题描述】:一般情况
在带宽、CPU 使用率和 GPU 使用率方面都非常密集的应用程序需要每秒从一个 GPU 传输大约 10-15GB 到另一个 GPU。它使用 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 16x 带宽。如果可以的话,我已经需要更进一步了。
避免多线程副本是不可能的,因为存在无法简单合并的独立生产者-消费者队列。
自旋锁性能下降似乎非常罕见(因为用例将它推到那么远),以至于在 Google 上,您找不到任何与自旋锁函数名称相关的结果。
正在升级到对映射 (Vulkan) 提供更多控制的 API,但它不适合作为短期修复。出于同样的原因,目前不能切换到更好的操作系统内核。
降低 CPU 负载也不起作用;除了(通常是微不足道且成本低廉的)缓冲区副本之外,还有太多工作要做。
问题
可以做什么?
我需要显着减少单个页面错误的数量。我知道已经映射到我的进程的缓冲区的地址和大小,我也知道内存还没有被提交。
如何确保以尽可能少的事务提交内存?
DX11 的特殊标志可以防止在取消映射后取消分配缓冲区,Windows API 强制在单个事务中提交,几乎任何东西都是受欢迎的。
当前状态
// In the processing threads
DX11DeferredContext->Map(..., &buffer)
std::memcpy(buffer, source, size);
DX11DeferredContext->Unmap(...);
【问题讨论】:
听起来你的 16 个线程加在一起的速度约为 400 M。相当低。你能确认你的申请没有超过这个吗?那里的选择内存消耗是多少?我想知道你是否有内存泄漏。 峰值消耗在 7-8GB 左右,但这是正常的,考虑到整个处理管道总共需要 >1s 的缓冲来补偿各种瓶颈。是的,它“只有”400MB,每秒 25 次。它工作得很好,直到基本 CPU 负载超过 50% 并且自旋锁的性能突然从 CPU 使用率的几乎 0 飙升到 ~40-50%。同时也会影响系统上的其他进程。 1.你的物理记忆是什么?你能杀死所有其他活动进程吗? 2. 猜测 #2,因为您看到 50% 的阈值,您可能会遇到一些超线程问题。你有多少物理核心? 8?你能禁用超线程吗?尝试在干净的机器上运行与物理 CPU 一样多的线程。 @Serge 16GB,2.5 到 4GB 基线,具体取决于 Visual Studio 是否也在运行。这不是交换,这是我检查的第一件事。在有和没有其他进程运行的情况下发生。 6 核,但是是的,超线程处于活动状态,我还没有考虑过尝试。周一会做,但可能会导致 CPU 性能成为瓶颈。 Win10 在许多线程导致页面错误时似乎存在问题(请参阅***.com/questions/45024029/…)。这花费大约。因素二。您的解决方法仍然是您能做的最好的。你应该在 MS 支持处开一张票,如果他们可以对那个在早期 Windows 版本中便宜得多的热锁做点什么。 【参考方案1】:当前的解决方法,简化的伪代码:
// 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 使用率的
总结?
当您预计 Windows 上会出现软故障时,请尽量将它们全部保留在同一个线程中。执行并行memcpy
本身是没有问题的,在某些情况下甚至需要充分利用内存带宽。但是,这只是在内存已经提交到 RAM 的情况下。 VirtualLock()
是确保这一点的最有效方式。
(除非您使用像 DirectX 这样将内存映射到进程的 API,否则您不太可能经常遇到未提交的内存。如果您只是使用标准 C++ new
或 malloc
,您的内存将被池化和回收无论如何,在你的过程中,所以软故障很少见。)
在使用 Windows 时,请确保避免任何形式的并发页面错误。
【讨论】:
以上是关于如何在 C++ 中急切提交分配的内存?的主要内容,如果未能解决你的问题,请参考以下文章