Ext*_*t3h 21 c++ memory windows multithreading directx-11
概况
对带宽,CPU使用率和GPU使用率极为密集的应用程序需要从一个GPU到另一个GPU每秒传输大约10-15GB.它使用DX11 API访问GPU,因此只有使用需要映射的缓冲区才能上传到GPU.上传一次发生在25MB的块中,并且16个线程同时将缓冲区写入映射的缓冲区.关于这一点,没有什么可以做的.如果不是针对以下错误,则写入的实际并发级别应该更低.
它是一个功能强大的工作站,配有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上,你不会找到旋转锁定功能名称的单一结果.
升级到可以更好地控制映射的API(Vulkan)正在进行中,但它不适合作为短期修复.出于同样的原因,切换到更好的OS内核目前不是一种选择.
减少CPU负载也不起作用; 除了(通常是微不足道的和便宜的)缓冲区副本之外,还有太多的工作需要完成.
问题
可以做些什么?
我需要显着减少单个页面故障的数量.我知道已映射到我的进程的缓冲区的地址和大小,我也知道内存尚未提交.
如何确保以尽可能少的事务提交内存?
DX11的异常标志可以防止在取消映射后取消分配缓冲区,Windows API强制在单个事务中提交,几乎任何东西都是受欢迎的.
目前的状态
// In the processing threads
{
DX11DeferredContext->Map(..., &buffer)
std::memcpy(buffer, source, size);
DX11DeferredContext->Unmap(...);
}
Run Code Online (Sandbox Code Playgroud)
Ext*_*t3h 12
当前的解决方法,简化的伪代码:
// 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();
}
Run Code Online (Sandbox Code Playgroud)
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++,new
或者malloc
您的内存在您的进程内汇集并回收,那么软故障就是如此罕见.)
在使用Windows时,请确保避免任何形式的并发页面错误.