针对小内存量命令列表的内存循环优化对于减少命令列表 DDI 函数调用之间的争用和降低命令列表所需的调用处理开销非常重要。 每个命令列表中固有的处理开销都很大。 此优化适用于命令列表,在这种情况下,命令列表所需的处理开销会占用命令列表所需的 CPU 时间和内存空间。 小内存量命令列表是指单个图形命令,如 CopyResource。 CopyResource 所需的内存量为两个指针。 不过,CopyResource 仍需要与大内存容量命令列表相同的命令列表调用处理量。 当小内存量命令列表频繁生成时,运行时调用驱动程序的 CreateCommandList、DestroyCommandList、CreateDeferredContext 和 DestroyDevice(D3D10) 函数(用于延迟上下文)所需的处理开销就变得越来越重要。 这里所指的内存是存放驱动程序数据结构的系统内存,其中包括 DDI 句柄内存。
驱动程序的 RecycleCommandList 函数必须在驱动程序句柄停止使用(但尚未删除)以及以前未使用的驱动程序句柄被重新使用时通知驱动程序。 此通知同时适用于命令列表和延迟上下文句柄。 驱动程序必须回收的唯一内存是 DDI 句柄指向的内存。 虽然 RecycleCommandList 的目标是回收与句柄相关联的内存,但为了提高效率,驱动程序可以完全灵活地选择回收哪些内存。 驱动程序无法更改即时上下文命令列表句柄指向的内存区域的大小。 此大小是 CalcPrivateCommandListSize 的返回值。 驱动程序也不能更改上下文命令列表本地句柄指向的内存区域的大小,该大小是 CalcDeferredContextHandleSize 的返回值。
驱动程序的 RecycleCreateCommandList 和 RecycleCreateDeferredContext DDI 函数必须以 E_OUTOFMEMORY HRESULT 值返回内存不足错误代码。 这些函数不会通过调用 pfnSetErrorCb 函数来提供此类错误代码。 通过这一驱动程序要求,运行时就不必使用全设备同步来监视这些创建类型驱动程序函数的即时上下文错误。 对于小内存量命令列表来说,注意这些错误会造成灾难性的争用。
驱动程序的 RecycleDestroyCommandList、RecycleCommandList 和 RecycleCreateCommandList 函数之间的区别非常重要。 它们的功能如下。
1. 小内存量命令列表的定义与挑战
核心特征
典型示例:CopyResource、SetRenderTarget 等简单命令,仅需存储 2个指针(源/目标资源句柄)。
问题:
- 与大命令列表(如复杂Draw调用)相同的 创建/销毁开销。
- 高频调用时,CreateCommandList/DestroyCommandList 的CPU开销占比显著。
性能瓶颈
| 操作 | 开销来源 | 优化目标 |
|---|---|---|
CreateCommandList |
内存分配、句柄初始化 | 复用预分配内存块 |
DestroyCommandList |
内存释放、线程同步 | 延迟回收或池化 |
| 命令记录 | 命令缓冲区填充 | 最小化元数据 |
2. 驱动回收函数职责与实现
关键函数分工
| 函数 | 触发时机 | 内存管理行为 |
|---|---|---|
RecycleCommandList |
命令列表执行完毕但句柄未销毁时 | 标记句柄关联内存为可复用,可选释放非句柄内存(如命令缓冲区)。 |
RecycleCreateCommandList |
创建新命令列表时复用旧句柄内存 | 返回复用内存的句柄,若内存不足则返回 E_OUTOFMEMORY(禁止 用 pfnSetErrorCb)。 |
RecycleDestroyCommandList |
命令列表句柄最终销毁时 | 彻底释放句柄及其所有内存,不可再复用。 |
实现示例
// 1. 命令列表内存池(线程安全无锁队列)
std::queue<CommandListMemoryBlock> g_CommandListPool;
// 2. RecycleCommandList:标记内存为可复用
void APIENTRY RecycleCommandList(D3D11DDI_HCOMMANDLIST hCommandList) {
CommandListMemoryBlock* pBlock = GetMemoryBlock(hCommandList);
g_CommandListPool.push(*pBlock); // 回收至池
}
// 3. RecycleCreateCommandList:从池中获取内存
HRESULT APIENTRY RecycleCreateCommandList(
D3D10DDI_HDEVICE hDevice,
D3D11DDI_HCOMMANDLIST* phCommandList
) {
if (g_CommandListPool.empty()) {
return E_OUTOFMEMORY; // 直接返回错误,无回调
}
*phCommandList = g_CommandListPool.front().hCommandList;
g_CommandListPool.pop();
return S_OK;
}
// 4. RecycleDestroyCommandList:彻底释放
void APIENTRY RecycleDestroyCommandList(D3D11DDI_HCOMMANDLIST hCommandList) {
FreeMemoryBlock(GetMemoryBlock(hCommandList));
}
3. 延迟上下文句柄的复用优化
特殊处理
CalcDeferredContextHandleSize:
驱动需返回固定大小(如 sizeof(DeferredContextData)),运行时严格依赖此值预分配内存。
复用限制:
延迟上下文句柄的内存区域大小不可变,但驱动可内部复用附属数据结构(如命令缓冲区)。
性能权衡
| 策略 | 优点 | 缺点 |
|---|---|---|
| 完全池化 | 零分配开销,极低延迟 | 内存占用较高 |
| 按需分配+部分复用 | 内存高效 | 高频调用时仍有分配压力 |
4. 内存不足处理规则
强制要求
RecycleCreate* 函数:
必须直接返回 E_OUTOFMEMORY,禁止使用 pfnSetErrorCb。
避免全设备同步,减少争用。
运行时响应:
应用收到 E_OUTOFMEMORY 后,应缩减命令列表规模或重建延迟上下文。
错误传播流程
sequenceDiagram
participant App
participant Runtime
participant Driver
App->>Runtime: FinishCommandList
Runtime->>Driver: RecycleCreateCommandList
alt 内存充足
Driver->>Runtime: 返回复用句柄
Runtime->>App: 返回S_OK
else 内存不足
Driver->>Runtime: 返回E_OUTOFMEMORY
Runtime->>App: 返回E_OUTOFMEMORY
end
5. 调试与调优
性能计数器
池命中率:
void LogPoolStats() {
DebugPrint("CommandList Pool Hit Rate: %.2f%%\n",
(g_ReuseCount / (g_ReuseCount + g_AllocCount)) * 100);
}
内存波动:
通过 ETW 追踪 RecycleCreateCommandList 的失败频率,调整池大小。
验证工具
PIX:捕获命令列表内存地址,验证复用行为。
自定义注入:强制模拟内存不足,测试应用健壮性。
6. 总结
高频小命令列表:通过内存池化降低创建/销毁开销。
严格大小约束:句柄内存区域大小由 Calc*Size 固定,驱动仅可优化内部附属内存。
无锁设计:回收与分配路径需线程安全,避免争用。
显式错误码:RecycleCreate* 直接返回 E_OUTOFMEMORY,确保低延迟。