C Code Optimization
Writing code that works is the first goal. Writing code that works fast and uses memory efficiently is the next. Code optimization in C means improving your program's speed, memory usage, or both — without changing what the program does.
C gives you more direct control over hardware than most languages, which means both greater optimization potential and greater responsibility.
Two Types of Optimization
| Type | Focus | Tools/Techniques |
|---|---|---|
| Speed (Time) | Make the program run faster | Algorithms, loops, cache, compiler flags |
| Memory (Space) | Use less RAM | Data types, bit fields, dynamic allocation |
Golden Rule: Always measure before optimizing. A guess about where slowness comes from is usually wrong. Profile first, then optimize.
1. Choose the Right Algorithm First
No micro-optimization can compensate for a poor algorithm. Sorting 1 million numbers with Bubble Sort (O(n²)) takes millions of operations. Using QuickSort (O(n log n)) takes thousands. That is a difference of hours vs milliseconds.
Algorithm complexity comparison for n = 1,000,000:
O(1) : 1 operation (constant — best)
O(log n) : ~20 operations (binary search)
O(n) : 1,000,000 operations (linear scan)
O(n log n) : ~20,000,000 (merge sort, quicksort)
O(n²) : 1,000,000,000,000 (bubble sort — terrible for large n)
2. Use the Correct Data Types
Choose the smallest data type that can hold your data. Smaller types mean less memory and faster cache access.
// Wasteful — using int for a small flag
int isActive = 1; // uses 4 bytes
// Efficient — use char or bit field
char isActive = 1; // uses 1 byte
unsigned char flag; // 1 byte, values 0-255
// For a table of grades (0-100), use unsigned char, not int:
unsigned char grades[1000]; // 1000 bytes
// vs:
int grades[1000]; // 4000 bytes — 4x more memory
3. Compiler Optimization Flags
The GCC compiler can automatically optimize your code. These flags instruct the compiler to apply various performance improvements to the output machine code.
| Flag | Level | Effect |
|---|---|---|
-O0 | None (default) | No optimization — fastest compilation, slowest binary |
-O1 | Basic | Simple optimizations, reasonable compile time |
-O2 | Standard | Most optimizations, recommended for production |
-O3 | Aggressive | Maximum speed, may increase binary size |
-Os | Size | Optimize for smallest binary size |
gcc -O2 -o fast_program program.c // recommended for most programs
4. Optimize Loops
Loops are where programs spend most of their time. Small improvements inside a loop multiply across every iteration.
Move Invariant Calculations Outside the Loop
// Slow: computes strlen on every iteration
for (int i = 0; i < strlen(str); i++) { ... }
// Fast: compute once, store in variable
int len = strlen(str);
for (int i = 0; i < len; i++) { ... }
Prefer Pre-increment Over Post-increment
// Post-increment creates a temporary copy (small overhead)
for (int i = 0; i < n; i++) { }
// Pre-increment — slightly faster (no temp copy)
for (int i = 0; i < n; ++i) { }
Loop Unrolling (Manual)
Processing multiple elements per iteration reduces loop overhead (fewer increments, fewer comparisons).
// Normal loop — overhead on every iteration
for (int i = 0; i < 8; i++)
a[i] = b[i] + c[i];
// Unrolled — processes 2 elements per iteration (half the loop overhead)
for (int i = 0; i < 8; i += 2) {
a[i] = b[i] + c[i];
a[i+1] = b[i+1] + c[i+1];
}
5. Memory Access Optimization (Cache Locality)
Modern CPUs load data in blocks called cache lines (typically 64 bytes). Accessing memory sequentially (linear) is much faster than jumping around randomly, because sequential access uses the cache efficiently.
int matrix[1000][1000];
// Fast: row-major access (sequential in memory — cache friendly)
for (int i = 0; i < 1000; i++)
for (int j = 0; j < 1000; j++)
sum += matrix[i][j]; // reads row by row ✓
// Slow: column-major access (jumps in memory — cache unfriendly)
for (int j = 0; j < 1000; j++)
for (int i = 0; i < 1000; i++)
sum += matrix[i][j]; // jumps 1000 integers each step ✗
6. Use Bitwise Operations for Fast Math
// Slow: arithmetic
int result = x * 2; // multiplication
int half = x / 4; // division
// Fast: bit shifts (single CPU instruction)
int result = x << 1; // multiply by 2
int half = x >> 2; // divide by 4
// Check even/odd without modulo
if (x & 1) printf("odd"); // faster than x % 2
7. Avoid Function Call Overhead with inline
For very small functions called many times, inline tells the compiler to copy the function body directly at the call site instead of making a function call.
// Regular function — call overhead on each use
int square(int x) { return x * x; }
// Inline — compiler inserts code directly at call site
static inline int square(int x) { return x * x; }
8. Prefer Stack Over Heap When Possible
Stack allocation is instantaneous — just moving a register. Heap allocation (malloc) involves a system call and is much slower. Use local arrays when the size is known and small.
// Slow for small sizes: heap allocation
int *arr = malloc(10 * sizeof(int));
// ... use arr ...
free(arr);
// Fast: stack allocation (if size is small and fixed)
int arr[10]; // instantly available, no cleanup needed
9. Profile Before Optimizing
Use gprof to find which functions consume the most time. Optimize those first.
gcc -pg -o program program.c # compile with profiling
./program # run — generates gmon.out
gprof program gmon.out > report.txt # generate report
The report shows each function's percentage of total execution time. Optimize the top consumers — fixing 1% of a 0.1% function gives nothing; fixing 50% of a 60% function gives huge gains.
Optimization Summary Table
| Technique | Benefit | Effort |
|---|---|---|
| Better algorithm | Massive | High (design change) |
| Compiler -O2 flag | Large (free) | None |
| Correct data types | Medium | Low |
| Loop invariants out | Medium | Low |
| Cache-friendly access | Large | Medium |
| Bitwise vs arithmetic | Small | Low |
| Stack vs heap | Medium | Low |
| inline functions | Small | Low |
Summary
C code optimization improves program speed and memory usage without changing behavior. Always choose the best algorithm first — this gives the largest gains. Enable compiler optimization with -O2 for free performance improvements without writing a single extra line. Use appropriate data types to reduce memory. Optimize loops by moving invariant calculations outside and using cache-friendly memory access patterns. Bitwise operations replace slow arithmetic for power-of-2 operations. Stack allocation is faster than heap for small, fixed-size data. Profile your program with gprof before optimizing — always spend effort on the slowest parts, not guesses. Optimization is a process of measurement, targeted change, and re-measurement, not a one-time event.
