如何在C ++中提前分配内存?

总体情况

对带宽,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时,请确保避免任何形式的并发页面错误。