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 likepushor stack-pointer-relativemovoperations. 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
ECXandEDX, 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
pushand restoring them withpopincurs 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 viaadd esp, 4*N. This adds an instruction at every call site, increasing binary footprint. However, caller clean-up allowscdeclto support variable argument lists (variadic functions likeprintf), 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.