Understanding cdecl and fastcall Overhead in C

Function calling conventions in C define the low-level contract for passing arguments, returning values, and managing the stack between callers and callees. The overhead associated with conventions such as cdecl and fastcall primarily consists of memory operations, register allocation and spilling, instruction counts in function prologues and epilogues, and stack-pointer cleanup. This article examines the exact hardware and software factors that generate calling convention overhead and compares how cdecl and fastcall balance these performance costs.

1. Memory Access and Stack Manipulation

The most prominent source of overhead in traditional calling conventions is interaction with the stack. Reading and writing to RAM, even when buffered by the L1 data cache, introduces latency compared to direct CPU register manipulation.

  • Stack Pushes and Pops: In cdecl, all arguments are pushed onto the call stack in right-to-left order using instructions like push or stack-pointer-relative mov operations. Each push requires modifying the stack pointer (esp/rsp) and writing to memory.
  • Cache Latency: While the top of the stack usually resides in the L1 CPU cache, cache hits still incur a latency of 3 to 4 clock cycles, whereas CPU registers are accessed in a single cycle or less via register renaming.

2. Register Utilization and Register Spilling

fastcall mitigates memory overhead by passing the first two (or more, depending on the compiler and architecture) pointer- or integer-sized arguments in registers—typically ECX and EDX on x86-32. However, passing parameters in registers introduces its own overhead:

  • Register Pressure: CPUs have a finite number of general-purpose registers (GPRs). When arguments occupy ECX and EDX, fewer registers are available for intermediate computations within the function.
  • Register Spilling: If the callee is computationally complex and requires more registers than are available, it must "spill" registers to the stack. This negates the performance benefit of register-based parameter passing.
  • Preservation Costs (Caller vs. Callee Saved): Calling conventions dictate whether the caller or callee must preserve register states. Saving registers to the stack with push and restoring them with pop incurs instruction and latency overhead.

3. Function Prologue and Epilogue Size

Every function invocation requires administrative instructions to establish and dismantle execution contexts:

  • Prologue Overhead: Functions often set up a standard stack frame (push ebp; mov ebp, esp; sub esp, N) to allocate local variables and preserve callee-saved registers.
  • Epilogue Overhead: Functions must restore callee-saved registers, reset the stack pointer (mov esp, ebp; pop ebp), and execute a return instruction.
  • Code Density: More instructions in prologues and epilogues increase binary size, which indirectly reduces instruction cache (I-cache) efficiency.

4. Stack Cleanup Responsibility

Conventions differ in which party is responsible for clearing arguments off the stack:

  • cdecl (Caller Clean-up): The calling function removes parameters from the stack after the callee returns, typically via add esp, 4*N. This adds an instruction at every call site, increasing binary footprint. However, caller clean-up allows cdecl to support variable argument lists (variadic functions like printf), because only the caller knows how many arguments were pushed.
  • fastcall (Callee Clean-up): The called function cleans up any remaining stack-allocated arguments using a return instruction that accepts an immediate value (e.g., ret 8). This eliminates the cleanup instruction at the call site, reducing code size, but prevents variadic parameter lists.

Direct Comparison: cdecl vs. fastcall

Overhead Metric cdecl fastcall
Argument Passing Memory-bound (Stack) Hybrid (Registers + Stack)
Initial Latency Higher (Memory writes) Lower (Register transfers)
Code Size at Call Site Larger (Requires manual stack adjustment) Smaller (Callee cleans via ret N)
Register Pressure Low (All GPRs free for caller/callee logic) High (GPRs consumed by arguments)
Variadic Support Yes No

Modern Context (x86-64)

In modern 64-bit computing, the historical distinction between 32-bit cdecl and fastcall is largely obsolete. Standard 64-bit ABIs—such as the System V AMD64 ABI (Linux/macOS) and the Microsoft x64 calling convention (Windows)—are essentially standardized, highly optimized variants of fastcall. System V passes up to six integer/pointer arguments in registers (RDI, RSI, RDX, RCX, R8, R9), and Windows passes four (RCX, RDX, R8, R9), reserving stack operations only for functions with numerous parameters.