Project 15: Performance-Optimized Data Structures

A suite of cache-efficient data structures with rigorous benchmarking and profiling.

Quick Reference

Attribute Value
Difficulty Level 5 - Master
Time Estimate 3-4 weeks
Main Programming Language C
Alternative Programming Languages None
Coolness Level Level 5 - Pure Magic
Business Potential Level 2 - Micro-SaaS
Prerequisites Data structures, memory layout, benchmarking basics
Key Topics Cache locality, data-oriented design, profiling

1. Learning Objectives

By completing this project, you will:

  1. Design data structures optimized for cache locality.
  2. Measure performance using rigorous benchmarking methods.
  3. Compare array-of-structs vs struct-of-arrays layouts.
  4. Implement and profile multiple data structures (hash table, deque, priority queue).
  5. Produce a performance report with clear conclusions.

2. All Theory Needed (Per-Concept Breakdown)

Concept 1: Cache Locality and Data-Oriented Design

Fundamentals

Modern CPU performance is dominated by memory hierarchy. Accessing data in cache is orders of magnitude faster than accessing main memory. Data-oriented design (DOD) focuses on arranging data to maximize cache locality and minimize cache misses. This often means using contiguous arrays, struct-of-arrays layouts, and avoiding pointer-heavy structures when performance matters.

Deep Dive into the concept

CPUs fetch memory in cache lines (typically 64 bytes). If your data structure stores data contiguously, iterating over it will likely hit the cache line already loaded, yielding high performance. If your data structure is pointer-heavy (linked lists, trees), each node may be on a different cache line, causing frequent cache misses. These misses can dominate runtime even if the algorithmic complexity is the same.

Data-oriented design starts with access patterns: what operations are most common, and what data is needed for them? If you frequently scan a list of objects and only need a few fields, a struct-of-arrays layout can reduce memory bandwidth by keeping only the relevant data contiguous. For example, instead of an array of structs {x,y,z,id}, you can store separate arrays x[], y[], z[], id[]. This improves cache utilization and can enable SIMD vectorization.

Cache locality is also influenced by alignment and padding. A struct that is 24 bytes may not align neatly with cache lines, causing inefficient packing. Reordering fields or padding to cache-line boundaries can improve performance. Prefetching can further reduce cache misses by loading data before it is used. However, over-optimization can waste memory or complicate code, so you must balance performance with maintainability.

In this project, you will implement multiple versions of data structures (e.g., linked list vs array-based queue) and measure the performance difference. You will design the data layout explicitly, reason about cache lines, and use profiling tools to confirm your intuition. The goal is to internalize how memory layout drives real-world performance.

To go deeper, relate locality to the CPU’s TLB and prefetcher. When working sets exceed cache size, TLB misses become significant, and page walks add overhead. Contiguous layouts help the TLB by keeping addresses within fewer pages. Stride matters: if your loop accesses every Nth element, you may defeat the prefetcher and cause cache misses. DOD encourages you to restructure loops to access data in the order it is stored, not the order it is conceptually organized. You can also add explicit prefetch instructions for predictable access patterns, but this is advanced and should be justified by profiling. Including a TLB and stride discussion makes the performance story more complete and closer to what professionals analyze in performance-critical systems.

To operationalize this concept in a real codebase, create a short checklist of invariants and a set of micro-experiments. Start with a minimal, deterministic test that isolates one rule or behavior, then vary a single parameter at a time (inputs, flags, platform, or data layout) and record the outcome. Keep a table of assumptions and validate them with assertions or static checks so violations are caught early. Whenever the concept touches the compiler or OS, capture tool output such as assembly, warnings, or system call traces and attach it to your lab notes. Finally, define explicit failure modes: what does a violation look like at runtime, and how would you detect it in logs or tests? This turns abstract theory into repeatable engineering practice and makes results comparable across machines and compiler versions.

How this fits on projects

Definitions & key terms

  • Cache line: The unit of data transfer between memory and CPU cache.
  • Locality: Access pattern where nearby data is accessed together.
  • AoS/SoA: Array-of-structs vs struct-of-arrays layouts.
  • DOD: Data-oriented design, organizing data for access patterns.

Mental model diagram (ASCII)

AoS: [x y z id][x y z id][x y z id]
SoA: [x x x x][y y y y][z z z z][id id id id]

How it works (step-by-step, with invariants and failure modes)

  1. Identify hot paths and data access patterns.
  2. Choose data layout (AoS vs SoA).
  3. Align and pack data to cache lines.
  4. Measure cache misses and performance.

Invariant: Hot data should be contiguous and aligned. Failure mode: Pointer-heavy layouts cause cache thrashing.

Minimal concrete example

typedef struct { float x, y, z; int id; } particle_t;

Common misconceptions

  • “Big-O is all that matters.” → Constants and cache dominate performance.
  • “Linked lists are always fast.” → They are often cache-unfriendly.
  • “SoA is always better.” → It depends on access patterns.

Check-your-understanding questions

  1. Why is cache locality important?
  2. What is the difference between AoS and SoA?
  3. Why can linked lists be slow?
  4. What is a cache line?
  5. How do you decide on a data layout?

Check-your-understanding answers

  1. Cache hits are much faster than memory accesses.
  2. AoS groups fields per object; SoA groups fields per field.
  3. Each node may be on a different cache line.
  4. The unit of data fetched into cache.
  5. By analyzing access patterns and hot paths.

Real-world applications

  • Game engines and physics simulations.
  • High-frequency trading systems.

Where you’ll apply it

References

  • “Data-Oriented Design” — Mike Acton (talks)
  • “Computer Systems: A Programmer’s Perspective” — cache chapters

Key insights

Performance is often determined by memory layout, not algorithmic complexity alone.

Summary

Cache locality and data-oriented design drive real-world performance. Choosing the right layout is the most impactful optimization for many data structures.

Homework/Exercises to practice the concept

  1. Implement an AoS and SoA version of a particle update loop.
  2. Measure cache misses with perf or valgrind --tool=cachegrind.
  3. Reorder struct fields to reduce padding.

Solutions to the homework/exercises

  1. SoA should be faster when accessing one field per loop.
  2. Cachegrind should show fewer misses for contiguous layouts.
  3. Order fields by decreasing alignment.

Concept 2: Benchmarking, Profiling, and Statistical Rigor

Fundamentals

Performance claims are meaningless without measurement. Benchmarking requires controlled experiments, warm-up runs, and statistical analysis. Profiling tools like perf and cachegrind reveal hotspots and cache behavior. A professional performance project must produce reproducible results with clear methodology.

Deep Dive into the concept

Benchmarking is not just running code and timing it once. It requires a stable environment: fixed CPU frequency, disabled turbo if needed, and minimized background processes. You must run multiple iterations and report averages and variance. Microbenchmarks are sensitive to noise, so tools like hyperfine help automate repeated runs and statistical summaries.

Profiling tells you where time is spent. perf can show CPU cycles, cache misses, branch mispredictions, and more. cachegrind simulates cache behavior and provides detailed statistics. For data structures, cache misses often dominate. A robust benchmarking setup includes both timing and hardware-counter metrics.

Another key point is input size and distribution. A hash table might perform well on uniform keys but poorly on clustered keys. You should design benchmark workloads that reflect realistic use cases and stress worst-case scenarios. Reporting must include input sizes, distribution assumptions, and hardware specs. This makes the results reproducible and meaningful.

In this project, you will build a benchmarking harness that runs data structure operations under controlled conditions, measures timing, and collects cache statistics. You will produce plots or tables that compare different implementations and justify why one is faster. This is professional performance engineering, not guesswork.

To operationalize this concept in a real codebase, create a short checklist of invariants and a set of micro-experiments. Start with a minimal, deterministic test that isolates one rule or behavior, then vary a single parameter at a time (inputs, flags, platform, or data layout) and record the outcome. Keep a table of assumptions and validate them with assertions or static checks so violations are caught early. Whenever the concept touches the compiler or OS, capture tool output such as assembly, warnings, or system call traces and attach it to your lab notes. Finally, define explicit failure modes: what does a violation look like at runtime, and how would you detect it in logs or tests? This turns abstract theory into repeatable engineering practice and makes results comparable across machines and compiler versions.

Another way to deepen understanding is to map the concept to a small decision table: list inputs, expected outcomes, and the assumptions that must hold. Create at least one negative test that violates an assumption and observe the failure mode, then document how you would detect it in production. Add a short trade-off note: what you gain by following the rule and what you pay in complexity or performance. Where possible, instrument the implementation with debug-only checks so violations are caught early without affecting release builds. If the concept admits multiple approaches, implement two and compare them; the act of measuring and documenting the difference is part of professional practice. This habit turns theoretical understanding into an engineering decision framework you can reuse across projects.

Finally, keep a short field guide that lists typical symptoms when this concept is violated and the quickest diagnostic tool to use. For example, link errors, sanitizer crashes, or unexpected timing. This makes the concept actionable when things break. Repeat the exercise for at least one cross-platform variant so you see which parts are portable and which are not. Over time, this builds intuition that is hard to gain from theory alone.

How this fits on projects

Definitions & key terms

  • Benchmark: Controlled performance measurement.
  • Profiling: Measuring where time and resources are spent.
  • Cache miss: Access that is not found in cache.
  • Variance: Variability in benchmark results.

Mental model diagram (ASCII)

benchmark -> repeats -> stats (mean, p95) -> report

How it works (step-by-step, with invariants and failure modes)

  1. Define workload and input sizes.
  2. Warm up and run multiple iterations.
  3. Collect timing and counters.
  4. Summarize with statistics.

Invariant: Benchmarks are repeatable under fixed conditions. Failure mode: Noise or biased inputs invalidate results.

Minimal concrete example

hyperfine './bench --size 1e6'

Common misconceptions

  • “One run is enough.” → Noise can dominate results.
  • “Fastest result is the truth.” → Use averages and variance.
  • “Benchmarks don’t need warm-up.” → They do for caches and branch predictors.

Check-your-understanding questions

  1. Why run multiple iterations?
  2. What is a cache miss and why does it matter?
  3. Why use perf counters?
  4. What is the difference between micro and macro benchmarks?
  5. Why document hardware specs?

Check-your-understanding answers

  1. To reduce noise and compute statistics.
  2. It stalls the CPU while data is fetched from memory.
  3. They reveal low-level performance bottlenecks.
  4. Micro tests isolated operations; macro tests full workloads.
  5. Performance depends on hardware configuration.

Real-world applications

  • Optimizing database indexes.
  • Performance tuning in systems and games.

Where you’ll apply it

References

  • “Systems Performance” — Brendan Gregg
  • perf and hyperfine documentation

Key insights

Performance engineering is measurement plus analysis, not intuition.

Summary

Benchmarking and profiling provide the evidence behind performance decisions. Rigorous methodology is essential for credible results.

Homework/Exercises to practice the concept

  1. Run a benchmark 30 times and compute mean/stddev.
  2. Use perf stat to collect cache misses.
  3. Compare two implementations with identical workloads.

Solutions to the homework/exercises

  1. Use a script to collect and summarize results.
  2. perf stat -e cache-misses.
  3. Present results in a table with variance.

3. Project Specification

3.1 What You Will Build

A set of cache-optimized data structures (hash table, deque, priority queue) with multiple layout variants, plus a benchmarking harness and performance report.

3.2 Functional Requirements

  1. Implementations: At least 3 data structures with two layout variants each.
  2. Benchmark harness: Deterministic workloads and timing.
  3. Profiling: Collect cache miss statistics.
  4. Comparisons: Report AoS vs SoA performance.
  5. Documentation: Explain design trade-offs.

3.3 Non-Functional Requirements

  • Performance: Demonstrate measurable improvements.
  • Reliability: Correctness verified by unit tests.
  • Usability: Benchmark harness easy to run.

3.4 Example Usage / Output

$ ./bench --structure hash --size 1e6
Throughput: 50M ops/sec
Cache-misses: 1.2%

3.5 Data Formats / Schemas / Protocols

Benchmark report (CSV):

structure,variant,ops_sec,cache_miss_pct

3.6 Edge Cases

  • Very small and very large sizes.
  • Pathological hash distributions.
  • High contention workloads.

3.7 Real World Outcome

What you will see:

  1. Benchmark reports comparing data layouts.
  2. Profiling data showing cache behavior.
  3. A clear summary of design trade-offs.

3.7.1 How to Run (Copy/Paste)

make
./bench --all > perf_report.csv

3.7.2 Golden Path Demo (Deterministic)

Run a fixed workload and verify results are within expected range.

3.7.3 If CLI: exact terminal transcript

$ ./bench --structure deque --size 100000
Throughput: 120M ops/sec
Exit: 0

Failure demo (deterministic):

$ ./bench --structure unknown
ERROR: unknown structure
Exit: 2

4. Solution Architecture

4.1 High-Level Design

+-------------------+
| data structures    |
+---------+---------+
          |
          v
+-------------------+     +-------------------+
| benchmark harness  | -->| performance report|
+-------------------+     +-------------------+

4.2 Key Components

| Component | Responsibility | Key Decisions | |———–|—————-|—————-| | Structures | Implement variants | AoS vs SoA | | Bench harness | Run workloads | Deterministic seeds | | Profiler | Collect stats | perf/cachegrind |

4.3 Data Structures (No Full Code)

typedef struct { uint32_t key; uint32_t value; } entry_t;

4.4 Algorithm Overview

  1. Initialize data structures.
  2. Run workload loops.
  3. Collect timing and cache stats.
  4. Generate report.

Complexity Analysis:

  • Time: O(n) for each workload
  • Space: O(n)

5. Implementation Guide

5.1 Development Environment Setup

clang -O3 -std=c23 -Wall -Wextra -Werror

5.2 Project Structure

perf-structures/
├── src/
│   ├── hash.c
│   ├── deque.c
│   └── bench.c
├── include/
├── benchmarks/
└── Makefile

5.3 The Core Question You’re Answering

“How do I design data structures that are fast on modern CPUs, not just in theory?”

5.4 Concepts You Must Understand First

  1. Cache locality and data layout.
  2. Profiling and benchmarking methodology.
  3. Algorithmic trade-offs.

5.5 Questions to Guide Your Design

  1. Which operations are on the hot path?
  2. How will you measure cache misses?
  3. How will you ensure benchmarks are fair?

5.6 Thinking Exercise

Predict which is faster: linked list traversal or array traversal.

5.7 The Interview Questions They’ll Ask

  1. Why are linked lists slower than arrays?
  2. What is SoA vs AoS?
  3. How do you measure cache behavior?

5.8 Hints in Layers

  • Hint 1: Start with a baseline implementation.
  • Hint 2: Add a cache-optimized variant.
  • Hint 3: Benchmark and compare results.

5.9 Books That Will Help

| Topic | Book | Chapter | |——-|——|———| | Performance | “Systems Performance” — Gregg | Ch. 3 |

5.10 Implementation Phases

Phase 1: Foundation (1 week)

  • Implement baseline data structures.
  • Checkpoint: Correctness tests pass.

Phase 2: Core Functionality (1 week)

  • Add optimized variants and benchmarks.
  • Checkpoint: Benchmarks run successfully.

Phase 3: Polish & Edge Cases (1 week)

  • Analyze and document results.
  • Checkpoint: Performance report complete.

5.11 Key Implementation Decisions

| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | Layout | AoS, SoA | both | Compare trade-offs | | Benchmark tool | custom, hyperfine | custom + hyperfine | deterministic + stats |


6. Testing Strategy

6.1 Test Categories

| Category | Purpose | Examples | |———|———|———-| | Unit tests | Correctness | insert/remove | | Integration tests | Full workloads | bench runs | | Performance tests | Cache behavior | perf stats |

6.2 Critical Test Cases

  1. Correctness under heavy insert/delete workloads.
  2. Cache miss rates for AoS vs SoA.
  3. Deterministic benchmark results.

6.3 Test Data

Seed: 12345

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

| Pitfall | Symptom | Solution | |——–|———|———-| | Unfair benchmarks | Misleading results | Use identical workloads | | Ignoring cache behavior | Slow performance | Measure cache misses | | Over-optimizing | Hard to maintain | Document trade-offs |

7.2 Debugging Strategies

  • Use perf stat and cachegrind.
  • Compare assembly for hot loops.

7.3 Performance Traps

Benchmarking with small inputs hides cache effects; use realistic sizes.


8. Extensions & Challenges

8.1 Beginner Extensions

  • Add a vectorized SIMD variant.

8.2 Intermediate Extensions

  • Add multi-threaded benchmarks.

8.3 Advanced Extensions

  • Implement lock-free variants.

9. Real-World Connections

9.1 Industry Applications

  • High-performance databases.
  • Game engines and simulation engines.
  • Folly and Abseil containers.
  • Judy arrays.

9.3 Interview Relevance

  • Cache locality and performance optimization are common topics.

10. Resources

10.1 Essential Reading

  • “Systems Performance” — Gregg
  • “Data-Oriented Design” (talks)

10.2 Video Resources

  • Talks on cache-aware programming

10.3 Tools & Documentation

  • perf, cachegrind, hyperfine

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain cache locality and SoA/AoS.
  • I can design benchmarks with low noise.
  • I can interpret profiling output.

11.2 Implementation

  • Data structures pass correctness tests.
  • Benchmarks are reproducible.
  • Performance report is complete.

11.3 Growth

  • I can justify design choices with data.
  • I can optimize code based on profiling.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • At least 3 data structures implemented.
  • Benchmarks and profiling results.
  • Report documenting trade-offs.

Full Completion:

  • All minimum criteria plus:
  • Multiple layout variants and analysis.

Excellence (Going Above & Beyond):

  • Lock-free or SIMD-optimized implementations with performance data.