Project 11.03 Section 11 ⚑ Embedded Relevance: Critical std::map std::unordered_map Red-Black Trees Hash Tables Real-Time Determinism Flat Maps

11.03 Red-Black Trees vs Hash Tables: Deterministic O(log N) vs Unpredictable O(1) Rehash Latency

Executive Summary: Comparing ordered Red-Black Trees (std::map) against bucket-based Hash Tables (std::unordered_map). We explore time complexity determinism, catastrophic rehash latency spikes, dynamic node allocation heap fragmentation, and why static flat maps are the standard embedded choice.

πŸ’» 1. Annotated Source Code

#include <iostream>
#include <map>
#include <unordered_map>
#include <string>
using namespace std;

int main() {
	cout << "=== std::map ===" << endl;

	map<string, int> orderedMap;
	orderedMap["banana"] = 3;
	orderedMap["apple"] = 5;
	orderedMap["cherry"] = 2;

	cout << "Contents of ordered map (keys will be sorted):" << endl;
	for (const auto& pair : orderedMap) {
		cout << pair.first<<": " << pair.second << endl;
	}

	cout << "\nLooking up 'apple' in orderedMap: ";
	auto it1 = orderedMap.find("apple");
	if (it1 != orderedMap.end()) {
		cout << "Found, value = " << it1->second << endl;
	}
	else {
		cout << "Not found!" << endl;
	}

	cout << endl << endl;
	cout << "=== std::unordered_map ===" << endl;

	unordered_map<string, int> unorderedMap;
	unorderedMap["banana"] = 3;
	unorderedMap["apple"] = 5;
	unorderedMap["cherry"] = 2;

	cout << "Contents of unordered map (no guaranteed order):" << endl;
	for (const auto& pair : unorderedMap) {
		cout << pair.first << ": " << pair.second << endl;
	}

	cout << "\nLooking up 'apple' in unorderedMap: ";
	auto it2 = unorderedMap.find("apple");

	if (it2 != unorderedMap.end()) {
		cout << "Found, value = " << it2->second << endl;
	}
	else {
		cout << "Not found." << endl;
	}

	return 0;
}

πŸ“ 2. Architecture & UML Class Model

πŸ“ std::map (Red-Black) vs std::unordered_map (Hash Table) Architecture
+ Public - Private # Protected
<<template class>> std::map<Key, Value> O(log N) Red-Black Tree
-_M_root : _Rb_tree_node*
+operator[](k: const Key&) : Value& [O(log N)]
+insert(p: pair<const Key, Value>) : pair<iterator, bool>
<<template class>> std::unordered_map<Key, Value> O(1) Hash Table (Unbounded Worst-Case)
-_M_buckets : _Hash_node** (Dynamic Bucket Array)
+operator[](k: const Key&) : Value& [O(1) Avg, O(N) Worst]
+rehash(n: size_t) : void
<<embedded-etl>> FlatSortedMap<Key, Value, N> Zero-Heap Contiguous Flat Map
+keys[N] : Key
+values[N] : Value
+count : size_t
+find(k: Key) : Value*[Binary Search O(log N)]
πŸ”— Architectural Relationships & Hierarchy
std::map<Key, Value> ─ ─ > embedded deterministic alternative ─ ─ > FlatSortedMap<Key, Value, N>

πŸ“š 3. Core C++ Concepts Deep-Dive

1. Data Structure Architecture

std::map is implemented as a Self-Balancing Red-Black Tree with strictly ordered keys. Search, insertion, and deletion are guaranteed $O(\log N)$ time.

std::unordered_map is implemented as a Hash Table with Chaining. Average lookup is $O(1)$, but worst-case lookup is $O(N)$ when hash collisions occur or when the table triggers a dynamic rehashing re-allocation.

⚑ 4. Embedded Systems & Hardware Reality

1. Real-Time Determinism: The Rehash Latency Spike Hazard

In hard real-time systems (e.g. braking systems, avionics), an operation must NEVER exceed its worst-case execution time (WCET). While std::unordered_map is fast on average, inserting an element that triggers a bucket table resize reallocates memory and re-hashes every existing entryβ€”causing latency spikes of several milliseconds!

2. Heap Fragmentation: 24 to 32 Bytes per Node

Both standard containers dynamically allocate individual heap nodes for every stored element (storing tree pointers: parent, left, right, color, or hash buckets). On microcontrollers with limited SRAM, this causes extreme memory fragmentation.

πŸ’‘ The Embedded Solution: Flat Maps (Sorted Arrays)

For small, fixed sets of keys (e.g. CAN message IDs, sensor calibration tables), use a contiguous sorted std::array<std::pair<K, V>, N> with std::lower_bound for $O(\log N)$ binary search, zero heap allocations, and 100% L1 cache locality!

πŸ’‘ 5. Production-Ready Embedded Refactoring

Here is an embedded compile-time Flash ROM lookup table using binary search on sorted array:

πŸ’‘ Production-Ready Refactor
#include <array>
#include <algorithm>
#include <iostream>
#include <string_view>

struct CanMessageDescriptor {
    uint32_t canId;
    std::string_view name;
};

// Stored directly in Flash ROM (.rodata) - ZERO RAM overhead!
constexpr std::array<CanMessageDescriptor, 4> CAN_LUT = {{
    {0x100, "Engine RPM"},
    {0x101, "Vehicle Speed"},
    {0x200, "Brake Pressure"},
    {0x305, "Battery Voltage"}
}};

std::string_view lookupCanName(uint32_t id) {
    auto it = std::lower_bound(CAN_LUT.begin(), CAN_LUT.end(), id,
        [](const CanMessageDescriptor& item, uint32_t target) {
            return item.canId < target;
        });

    if (it != CAN_LUT.end() && it->canId == id) {
        return it->name;
    }
    return "Unknown ID";
}

πŸ“ Knowledge Verification Quiz

Test your understanding of the C++ concepts and embedded microcontroller trade-offs covered in this guide. Click any option for instant feedback.

Q1. Why is std::map often favored over std::unordered_map in hard real-time mission-critical systems?
A std::map is faster in average lookup than std::unordered_map.
B std::map provides guaranteed, predictable O(log N) worst-case timing without sudden rehashing pauses.
C std::map stores all elements in CPU registers.
D std::unordered_map does not support string keys.
Detailed Explanation: std::map guarantees $O(\log N)$ worst-case time complexity, whereas std::unordered_map can spike to $O(N)$ during hash collisions and bucket table reallocations, violating real-time deadlines.
Q2. What is the primary memory drawback of both std::map and std::unordered_map on memory-constrained microcontrollers?
A They store all data in non-volatile ROM.
B They allocate separate heap nodes for each inserted element, leading to severe RAM fragmentation.
C They cannot store more than 16 elements.
D They disable compiler inlining.
Detailed Explanation: Both node-based containers invoke malloc for each element to allocate tree nodes or linked list collision nodes, causing severe SRAM fragmentation and pointer overhead.
Q3. What embedded idiom offers O(log N) lookup with zero dynamic allocation and maximum cache locality?
A A sorted `std::array` or `etl::vector` queried via `std::lower_bound` (Flat Map).
B A circular singly linked list.
C A global raw `void*` array.
D A recursive switch statement.
Detailed Explanation: A Flat Map (contiguous array sorted by key) performs binary search in $O(\log N)$ time, requires zero dynamic allocation, and delivers optimal CPU cache locality.