A high-performance General Matrix Multiplication (GEMM) kernel engineered from scratch in C++. This project demonstrates the evolution of a compute-intensive kernel from a naive
Achieved ~89 GFLOPS on an AMD Zen 3 (Ryzen 5 5625U), reaching ~41% of the hardware's Theoretical Peak Performance by exploiting memory hierarchy, SIMD vectorization, and register-level blocking.
| Version | Description | Time (s) | GFLOPS | Speedup |
|---|---|---|---|---|
| v1. Naive | Standard Triple Loop | 6.82 s | 0.31 | 1.0x |
| v2. Reordered | Memory-Aware (i-k-j) | 0.1152 s | 18.63 | ~59x |
| v3. OpenMP | Multithreaded + Tiling | 0.0276 s | 77.83 | ~247x |
| v4. Ultimate | AVX2 + Register Blocking | 0.024 s | 89.38 | 283x |
This project is structured as a series of incremental optimizations, identifying specific hardware bottlenecks at each stage and resolving them using Systems Engineering principles.
- Bottleneck: The Naive implementation (
i-j-kloop order) accesses Matrix B in column-major order. Since C++ stores arrays in row-major order, this caused a Cache Miss on almost every access, fetching entire cache lines just to use a single double. - Solution: Reordered loops to
i-k-j. This accesses Matrix B sequentially (Stride-1), allowing the CPU's hardware prefetcher to load data into L1 cache efficiently. - Result: 59x Speedup purely from memory access pattern changes.
- Bottleneck: A single core cannot saturate the memory bandwidth or compute potential of modern CPUs.
- Solution: Implemented OpenMP threading with
collapse(2)to parallelize the outer loops. - Engineering Challenge: Identified a race condition in the initial
i-k-jparallelization where multiple threads fought for the sameC[i][j]accumulator. Resolved this by parallelizing the output coordinates (iandj), giving each thread a disjoint block of the Result Matrix to compute. - Result: ~247x Speedup (Scaling linearly with core count).
- Bottleneck: Even with cache blocking (
BS=32/40), the CPU spent more cycles loading data from L1 Cache into Registers than actually performing math. This is the classic "Von Neumann Bottleneck" at the L1 level. - Solution: Register Blocking (K-Unrolling) with AVX2 Intrinsics.
- SIMD: Used
_mm256_fmadd_pd(Fused Multiply-Add) to process 4 doubles per instruction. - Register Blocking: Unrolled the K-loop by 4. Instead of
Load C -> Math -> Store C, the kernel loads a vector of C into a YMM Register and keeps it there while accumulating results from 4 different K-steps. - Impact: Reduced L1 Cache load/store traffic by 75%, increasing Arithmetic Intensity and ensuring the FPUs (Floating Point Units) are fed constantly.
- SIMD: Used
- Result: 283x Speedup, achieving 89.38 GFLOPS.
The kernel uses a Block Size (BS) of 40.
-
Why 40? A cache line is 64 bytes (8 doubles). A block width of 40 doubles consumes exactly 5 cache lines (
$40/8 = 5$ ), ensuring clean memory alignment and minimizing "split loads" across cache boundaries. - L2 Residence: The blocking strategy ensures the working set fits comfortably within the 1.25MB L2 cache of the AMD zen 3 architecture.
The core computational kernel allows the CPU to perform 16 floating-point operations per cycle per core (assuming FMA throughput).
// Example of the Inner Micro-Kernel
// We hold 'vec_C' in a register to minimize memory access
for (; r < k_limit - 3; r += 4) {
// Broadcast A values
__m256d vec_A0 = _mm256_set1_pd(A_row[r]);
// ...
// Perform Fused Multiply-Add on 4 streams at once
vec_C = _mm256_fmadd_pd(vec_A0, vec_B0, vec_C);
// ...
}-
GCC or Clang with OpenMP support.
-
x86-64 CPU with AVX2 support (Haswell or newer).
# -O3: Maximum optimization
# -march=native: Enable AVX2/FMA instructions specific to your CPU
# -fopenmp: Enable multithreading
g++ -O3 -march=native -fopenmp Source.cpp -o gemm_bench -L/usr/local/lib -I/usr/local/include -lbenchmark_main -lbenchmark -lpthread
export OMP_NUM_THREADS=$(nproc)
./gemm_bench
Efficiency: The kernel achieves ~41% of the theoretical hardware limit. The remaining gap is attributed to the thermal throttling characteristic of mobile SKUs running dense AVX2 workloads and the lack of Assembly-level prefetching.
Aditya Kumar
Based on principles from "Computer Systems: A Programmer's Perspective" (Bryant & O'Hallaron). README.md formatted by Gemini.