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 ExecuteCommandLists call 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 via ExecuteBundle with minimal driver and GPU processing overhead.