总体情况
对带宽,CPU使用率和GPU使用率都非常敏感的应用程序需要每秒从一个GPU向另一个GPU传输大约10-15GB的数据。 它使用DX11 API来访问GPU,因此上传到GPU只能在需要映射每个单独上传的缓冲区时发生。 上传一次以25MB的大小发生,16个线程同时向缓冲区写入缓冲区。 关于这一点,没有什么可以做的。 写入的实际并发级别应该较低,如果它不是以下错误。
这是一个带有3 Pascal GPU,高端Haswell处理器和四通道RAM的强大工作站。 在硬件上没有太多可以改进的地方。 它运行的是Windows 10的桌面版本。
实际问题
一旦我传递了大约50%的CPU负载, MmPageFault()
某些东西(在Windows内核中,当访问已映射到您的地址空间但尚未被操作系统提交的内存时调用)破坏严重,其余50% MmPageFault()
的旋转锁正在浪费CPU负载。 CPU使用率达到100%,应用程序性能完全降低。
我必须假设,这是由于每秒需要分配给进程的大量内存,而且每次DX11缓冲区未映射时,这些内存也完全没有映射到进程中。 相应地,实际上每秒有数千次对MmPageFault()
的调用,随着memcpy()
被顺序写入缓冲区而顺序发生。 每遇到一个未提交的页面。
一旦CPU负载超过50%,保护页面pipe理的Windows核心中的乐观旋转locking会完全降低性能。
注意事项
缓冲区由DX11驱动程序分配。 没有什么可以调整分配策略。 使用不同的内存API,特别是重复使用是不可能的。
调用DX11 API(映射/取消映射缓冲区)全部来自单个线程。 实际的复制操作可能发生在multithreading上,而不是系统中的虚拟处理器。
减less内存带宽的要求是不可能的。 这是一个实时应用程序。 事实上,硬性限制目前是主要GPU的PCIe 3.0 16x带宽。 如果可以的话,我已经需要进一步推进了。
避免使用multithreading副本是不可能的,因为有独立的生产者 – 消费者队列,这些队列不能被轻易地合并。
自旋锁性能的下降似乎是非常罕见的(因为使用案例推得这么远),在Google上,你不会发现自旋锁函数名称的单一结果。
升级到对映射(Vulkan)进行更多控制的API正在进行中,但它不适合作为短期修复。 由于相同的原因,切换到更好的操作系统内核目前不是一种select。
减lessCPU负载也不起作用; 除了(通常微不足道的和廉价的)缓冲拷贝外,还有太多的工作需要完成。
问题
可以做什么?
我需要显着减less个别页面错误的数量。 我知道映射到我的进程的缓冲区的地址和大小,我也知道内存还没有被提交。
我怎样才能确保尽可能less的交易提交内存?
DX11的exception标志,它可以防止取消映射后的缓冲区解除分配,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()
函数的调用是可选的,当地址范围从进程中取消映射时,它隐式地发生(并且没有额外的成本)。 (如果显式调用,则花费大约为锁定成本的三分之一。)
为了使VirtualLock()
可以工作,需要首先调用SetProcessWorkingSetSize()
,因为由VirtualLock()
锁定的所有内存区域的总和不能超过为进程配置的最小工作集大小。 将“最小”工作集大小设置为高于进程的基准内存占用空间的大小没有副作用,除非系统实际上正在交换,否则您的进程将不会消耗比实际工作集大小更多的RAM。
只要使用VirtualLock()
,虽然在单独的线程中,并且使用延迟的DX11上下文来进行Map
/ Unmap
调用,但是立刻将性能损失从40-50%降低到稍微可接受的15%。
放弃延迟上下文的使用,并且仅仅触发所有软错误, 以及在单个线程上取消映射时相应的解除分配 ,都提供了必要的性能提升。 这个自旋锁的总成本现在已经下降到总CPU使用率的1%以下。
概要?
当你期望在Windows上出现软故障时,尽量把它们全部放在同一个线程中。 执行并行memcpy
本身是没有问题的,在某些情况下甚至需要充分利用内存带宽。 但是,只有在内存已经被提交到RAM的情况下。 VirtualLock()
是确保最有效的方法。
(除非你正在使用DirectX这样的API来将内存映射到你的进程中,否则你不可能频繁地遇到未提交的内存,如果你只是使用标准的C ++ new
或者malloc
你的内存就会在你的进程中被合并和回收,缺点是罕见的。)
使用Windows时,请确保避免任何形式的并发页面错误。