Welcome to our next lesson. In our previous sessions, we've honed our skills in writing efficient C++ code, first by mastering resource ownership with std::move, and then by creating flexible, stateful logic with lambda expressions. We are now at a point where we can write clean, modern, and efficient C++ operations. This lesson shifts our focus to the data that these operations will manipulate.
The performance of your whiteboard application won't just depend on clever algorithms; it will be fundamentally dictated by how you choose to store and access your data. Today, we will dive deep into the core data structures of the C++ Standard Library. Our goal is to compare the performance characteristics and memory layouts of std::vector, std::list, and std::unordered_map. More importantly, we'll explore why they perform the way they do, connecting their design to the underlying reality of modern CPU architecture. This understanding is critical for anyone interested in low-level, high-performance systems programming.
Performance is More Than Big-O: The Primacy of the CPU Cache
As a computer science student, you are well-versed in Big-O notation for analyzing algorithmic complexity. A std::list offers O(1) insertion in the middle, while a std::vector is O(n). An std::unordered_map offers O(1) lookups, while a sorted std::vector is O(log n). Based on theory alone, the choices might seem clear for certain tasks. However, in high-performance computing, the physical layout of data in memory often matters more than theoretical complexity.
Modern CPUs are orders of magnitude faster than main memory. To bridge this gap, they use multiple levels of small, extremely fast caches (L1, L2, L3). When the CPU needs data, it looks in L1 first. If it's not there (a "cache miss"), it looks in L2, then L3, and finally, resorts to the vastly slower main memory. Each cache miss incurs a significant performance penalty.
The key to performance is therefore data locality: organizing your data so that when you access one piece of it, the next piece you need is already in a fast cache.
In a landmark talk, Chandler Carruth, a lead developer for the Clang compiler, powerfully articulates this principle. He argues that while algorithms provide efficiency (doing less work), data structures provide performance (doing the same work faster).
CppCon 2014: Chandler Carruth "Efficiency with Algorithms, Performance with Data Structures"
This segment from Chandler Carruth's CppCon talk, "Efficiency with Algorithms, Performance with Data Structures," introduces the core idea for our lesson and explains the memory hierarchy that dictates modern software performance.
First, watch his thesis statement from this clip. Then, jump to the section where he explains the memory architecture of a modern CPU. Pay close attention to the table of latencies and his conclusion: the only way to solve the memory bottleneck is to get your data into the cache.
With this fundamental principle in mind, let's analyze our three containers.
std::vector: Contiguous, Cache-Friendly, and the Default Choice
A std::vector stores its elements in a single, contiguous block of memory on the heap. This is its defining feature and the source of its exceptional performance.

Memory Layout:
- A single dynamic array. If you have a
std::vector<int>, the integers are packed tightly together in memory:[int, int, int, ...].
Performance Characteristics:
- Access and Iteration: Because the data is contiguous, iterating through a
std::vectoris the most cache-friendly operation possible. When you accessvec[i], the CPU's prefetcher loads the subsequent elements (vec[i+1],vec[i+2], etc.) into the cache lines automatically. This makes sequential access incredibly fast. Random access via[]is also a true O(1) operation, involving simple pointer arithmetic. - Insertion (at the end):
push_backis amortized O(1). When the vector's internal capacity is exceeded, it allocates a new, larger block of memory (typically 1.5x to 2x the old size), moves all existing elements, and then adds the new one. While a singlepush_backcan be expensive, these reallocations happen infrequently. You can eliminate them entirely by using.reserve()if you know the approximate number of elements beforehand. - Insertion/Deletion (middle/front): This is the vector's primary weakness. An insertion or deletion requires shifting all subsequent elements, resulting in an O(n) operation.
- Destruction: Deallocating a vector is extremely fast. It involves calling the destructor for each element and then making a single call to free the entire memory block.
The following article provides excellent benchmarks that underscore std::vector's dominance.
Using C++ containers efficiently - Quasar.ai
This article from Quasar AI gives a practical, no-nonsense look at container performance.
Start by reading the initial benchmark under "Inserting and getting data". The results are striking. Then, read the section "Why? Why? Why!!!", which explains the performance differences in terms of memory allocation and continuity. Finally, review the points under "The ideal case", which summarize what hardware prefers.
For your whiteboard, where you will frequently iterate over all strokes to render them, std::vector is the obvious starting choice for storing stroke data.
std::list: The Cost of Flexibility
A std::list is a doubly-linked list. Its memory layout is the polar opposite of a std::vector.
Memory Layout:
- A collection of non-contiguous nodes scattered across the heap. Each node contains one element and two pointers (to the previous and next nodes).
Performance Characteristics:
- Access and Iteration: This is where
std::listpays a heavy price. To get to the Nth element, you must traverse N-1 nodes. Each traversal involves chasing a pointer, which almost guarantees a cache miss. The nodes are not located near each other in memory, defeating the CPU's prefetcher. Iterating over a list is dramatically slower than iterating over a vector. - Insertion/Deletion: This is the list's theoretical strength. Once you have an iterator pointing to a location, inserting or removing a node is an O(1) operation that only involves rearranging a few pointers.
- Iterator Invalidation: This is the most compelling reason to use
std::list. Inserting or deleting elements does not invalidate iterators or pointers to other elements in the list. This property can be crucial in complex data structures where you need to maintain stable references to elements while the container is being modified. - Destruction: Destroying a list requires visiting every single node to deallocate it individually, resulting in N deallocation calls.
Chandler Carruth offers a visceral critique of node-based data structures like std::list.
CppCon 2014: Chandler Carruth "Efficiency with Algorithms, Performance with Data Structures"
We return to the CppCon talk for a direct analysis of std::list.
Watch the segment from this timestamp. He explains precisely why the "each node is separately allocated" property is a "killer" for performance. His rule of thumb is powerful: if you traverse the data structure on the same order of magnitude as you update it, you should never use a linked list.
For your project, using a std::list to store strokes would be catastrophic for rendering performance. Its only potential use might be in a very specific scenario, like managing an LRU cache where iterator stability is paramount, but even then, other solutions are often better.
std::unordered_map: Fast... With Caveats
When you need to associate a key with a value for fast lookups, std::unordered_map is the standard tool. It's a hash table.
Memory Layout:
- The C++ standard's requirements for
std::unordered_map's interface strongly encourage an implementation based on separate chaining. This typically means:- An array of "buckets."
- Each bucket is a pointer to the head of a linked list of nodes.
- Each node contains the key-value pair.
- When a key is hashed, it maps to a bucket. If multiple keys hash to the same bucket (a "collision"), they are added to that bucket's linked list.
Performance Characteristics:
- Access: On average, this is an O(1) operation. It involves:
- Hashing the key.
- Using the hash to index into the bucket array.
- Chasing the pointer to the node (a potential cache miss).
- If there are collisions, traversing a short linked list.
- In the worst case (many collisions), access time degrades to O(n).
- Insertion/Deletion: Also average O(1). Like a vector, an
unordered_mapmay need to rehash (resize its bucket array and re-insert all elements) if itsload_factoris exceeded. This is an expensive operation. Using.reserve()is highly recommended. - Iteration: The order of elements is not guaranteed and depends on the hash values and insertion order. Iterating the whole map can be cache-inefficient as you are effectively jumping around a series of linked lists.

While unordered_map is vastly superior to a linear scan of a vector for lookups in a large dataset, its "O(1)" performance is not free. The constant factor includes hashing and at least one pointer indirection.
Again, Chandler Carruth explains why the standard unordered_map is frustrating and how better implementations (using open addressing) exist.
CppCon 2014: Chandler Carruth "Efficiency with Algorithms, Performance with Data Structures"
This final clip from the talk discusses associative containers.
Listen to his critique of both std::map and std::unordered_map starting from this point. He explains that std::unordered_map often devolves into a collection of linked lists, re-introducing the cache-miss problems we sought to avoid.
For a more formal and structured overview of the properties of these containers, the following video provides a clear catalogue.
Back to Basics: Classic STL - Bob Steagall - CppCon 2021
This clip from Bob Steagall's "Back to Basics: Classic STL" at CppCon 2021 provides a concise summary of each container's interface and guaranteed properties.
Watch the segment from this part of the talk. He systematically covers vector, list, map, and unordered_map, detailing their underlying structures, iterator types, and complexity guarantees. This complements the performance-focused analysis with the formal C++ standard perspective.
Summary and Practical Guidelines
Let's consolidate this into a table for quick reference.
| Feature | std::vector<T> |
std::list<T> |
std::unordered_map<K, V> |
|---|---|---|---|
| Memory Layout | Contiguous array | Dispersed nodes (doubly-linked) | Array of pointers to linked lists of nodes (typically) |
| Cache Friendliness | Excellent | Terrible | Poor to Moderate |
| Random Access | O(1) (fastest) | O(n) | Not applicable (key-based access) |
| Key-based Lookup | O(n) (linear search) | O(n) | O(1) average, O(n) worst-case |
| Insertion/Deletion | O(1) at end, O(n) at middle | O(1) anywhere (with iterator) | O(1) average |
| Iterator Invalidation | Invalidated on reallocation or middle insert/erase | Not invalidated (except for deleted element) | Not invalidated, but rehash invalidates everything |
| Primary Use Case | Default container. Sequences, fast iteration. | When iterator stability is a hard requirement. | Fast key-value lookups. Caches, symbol tables. |
Conclusion
You now have a deep, hardware-aware understanding of the C++ standard library's most fundamental containers. The choice of data structure is a critical, low-level design decision that has profound implications for performance. For your whiteboard project, this knowledge directly translates into concrete choices: std::vector for storing drawable objects like strokes due to its phenomenal iteration speed, and std::unordered_map for any situation where you need to quickly find an object by a unique ID.
Key Takeaways:
- Performance is driven by data locality. Minimizing cache misses is often more important than theoretical algorithmic complexity.
std::vectoris the default choice. Its contiguous memory layout makes it unbeatable for iteration and most common use cases. Always start here and prove you need something else.std::listis a niche container. Its primary valid use case is when you have an absolute requirement for iterator stability during modification. The performance cost for iteration is immense.std::unordered_mapprovides fast key-based lookups but is not "free." Its O(1) average complexity hides the cost of hashing and pointer indirection, which can lead to cache misses.
With this foundation, we are ready to move from the abstract world of data structures to the concrete world of graphics. In our next lesson, we will begin the "Graphics Backend & Windowing" module by using the SDL2 library to create our first hardware-accelerated window, setting the stage for bringing your whiteboard application to life.