DirectX 12 Small Command List Performance Costs
In DirectX 12, developers are granted low-level control over work generation and submission to maximize multithreaded rendering performance. However, closing and executing command lists that contain only a handful of draw calls introduces severe performance penalties across both the CPU and GPU. Rather than improving parallel efficiency, dispatching micro-sized command lists leads to high API submission overhead, GPU command processor starvation, memory allocation inefficiencies, and pipeline bubbles that negate the advantages of explicit rendering control.
CPU Submission Overhead
The primary CPU-side bottleneck stems from the
ID3D12CommandQueue::ExecuteCommandLists API call. While
DirectX 12 reduces driver overhead compared to DirectX 11, submission is
not free. Each call to ExecuteCommandLists requires the
user-mode driver and the operating system's kernel-mode graphics
subsystem (D3D runtime and OS kernel) to track synchronization
primitives, handle fence updates, and validate submission packages.
Submitting dozens or hundreds of command lists containing only a few
draw calls shifts the CPU cost from work recording to submission
management, often consuming more CPU time than the actual generation of
the draw commands.
GPU Front-End and Command Processor Starvation
Modern graphics processing units rely on dedicated front-end command processors (CPs) to fetch, decode, and dispatch command streams to compute and rasterization hardware. Executing a command list requires the GPU to:
- Read and parse command buffer headers.
- Set up internal state tables.
- Execute preemption or context-switching mechanisms if needed.
When a command list contains very few draw calls, the time the command processor spends parsing preamble instructions and transitioning between command streams exceeds the time spent executing the actual work. This starves the shader cores, creating "execution bubbles"—intervals where the execution units idle while waiting for the next command buffer to be fetched and decoded.
Loss of Hardware State Coherence
Command lists are completely self-contained execution units. Inherited state between command lists is not maintained (with minor exceptions for certain descriptors), meaning that every command list must explicitly bind the required root signatures, pipeline state objects (PSOs), and descriptor tables. When work is fragmented into micro-lists:
- Redundant state changes cannot be optimized out across boundaries.
- The GPU must re-validate and frequently reload root arguments and shader stages.
- Hardware caches containing pipeline setup and primitive configurations are invalidated more frequently, lowering overall cache hit rates.
Command Allocator Contention and Overhead
Command lists rely on ID3D12CommandAllocator to allocate
memory for recorded commands. Using numerous tiny command lists
complicates memory management. Command allocators request memory from
the operating system in larger pages (typically 64 KB or larger).
Populating these allocators with only minimal commands leads to
fragmentation and high memory churn, as allocators cannot reclaim
internal memory until the entire allocator is reset after GPU
retirement.
Mitigation Strategies
To maintain high throughput and avoid submission overhead, rendering systems should adhere to the following architectures:
- Batch Command Lists by Logical Pass: A single command list should generally contain hundreds to thousands of draw calls, capturing an entire render pass or a substantial subset of a pass recorded across worker threads.
- Consolidate Submissions: If multiple command lists
must be recorded in parallel, batch them into an array and submit them
in a single
ExecuteCommandListscall rather than executing them individually. - Use Direct3D 12 Bundles: For small groups of
recurring draw calls, use
D3D12_COMMAND_LIST_TYPE_BUNDLE. Bundles compile and pre-bake draw calls and state setups so they can be executed inside a parent command list viaExecuteBundlewith minimal driver and GPU processing overhead.