<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>STL | Ziji's Homepage</title><link>https://zijishi.xyz/tag/stl/</link><atom:link href="https://zijishi.xyz/tag/stl/index.xml" rel="self" type="application/rss+xml"/><description>STL</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><copyright>Ziji Shi © 2025</copyright><lastBuildDate>Wed, 19 Feb 2025 23:20:07 +0800</lastBuildDate><image><url>https://zijishi.xyz/media/icon_hu_926934747de47144.png</url><title>STL</title><link>https://zijishi.xyz/tag/stl/</link></image><item><title>Understanding Performance of C++ from the Implementation Perspective (2) : STL Containers</title><link>https://zijishi.xyz/post/cpp/an-incomplete-intro-to-stl-from-implementation-perspective/</link><pubDate>Wed, 19 Feb 2025 23:20:07 +0800</pubDate><guid>https://zijishi.xyz/post/cpp/an-incomplete-intro-to-stl-from-implementation-perspective/</guid><description>&lt;p&gt;One thing about C++ that attracts me is the flexibility to control the program. Therefore, I am writing a series of blogs based on my understanding of highly efficient C++ code. They come from various sources, including CppCon videos, StackOverflow, and the C++ standard. I have also benchmarked some of the claims. I hope this will be helpful to you.&lt;/p&gt;
&lt;h2 id="rule-of-thumb"&gt;Rule of Thumb&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;If &lt;code&gt;unordered&lt;/code&gt; exists in an STL container name, it is almost certainly implemented via a hash table.&lt;/li&gt;
&lt;li&gt;When in doubt, use &lt;code&gt;vector&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id="associative-containers-an-overview"&gt;Associative Containers: An Overview&lt;/h2&gt;
&lt;p&gt;STL provides two families of associative containers:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Container&lt;/th&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;th&gt;Ordered?&lt;/th&gt;
&lt;th&gt;Average Lookup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Red-black tree&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;map&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Red-black tree&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unordered_set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hash table&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unordered_map&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hash table&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The core trade-off is &lt;strong&gt;order vs. speed&lt;/strong&gt;. Tree-based containers maintain sorted order and guarantee O(log n) worst-case; hash-based containers give O(1) average but have no ordering and worst-case O(n) on hash collisions.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="unordered_set-and-unordered_map"&gt;&lt;code&gt;unordered_set&lt;/code&gt; and &lt;code&gt;unordered_map&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Both are implemented as a &lt;strong&gt;hash table&lt;/strong&gt; (open addressing or separate chaining depending on the STL implementation; most use separate chaining with a bucket array).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Complexity:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Average&lt;/th&gt;
&lt;th&gt;Worst case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Insert&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lookup&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The worst case occurs when all keys hash to the same bucket (degenerate collision). In practice this is rare with a good hash function and reasonable load factor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When to use:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need fast existence checks or key-value lookup and don&amp;rsquo;t care about order.&lt;/li&gt;
&lt;li&gt;Keys are primitive types or types for which a hash is readily available.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="defining-a-custom-hash"&gt;Defining a Custom Hash&lt;/h3&gt;
&lt;p&gt;For primitive types (&lt;code&gt;int&lt;/code&gt;, &lt;code&gt;size_t&lt;/code&gt;, etc.) the standard library provides hash specializations automatically. For custom types such as &lt;code&gt;pair&amp;lt;int,int&amp;gt;&lt;/code&gt; or a struct, you must supply your own.&lt;/p&gt;
&lt;p&gt;A simple and effective polynomial hash for 2D or 3D indices:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cpp"&gt;struct PairHash {
size_t operator()(const pair&amp;lt;int,int&amp;gt;&amp;amp; p) const {
size_t h = 17;
h = h * 53 + hash&amp;lt;int&amp;gt;{}(p.first);
h = h * 53 + hash&amp;lt;int&amp;gt;{}(p.second);
return h;
}
};
unordered_set&amp;lt;pair&amp;lt;int,int&amp;gt;, PairHash&amp;gt; visited;
unordered_map&amp;lt;pair&amp;lt;int,int&amp;gt;, int, PairHash&amp;gt; dist;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a 3D key, extend the pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cpp"&gt;struct TripleHash {
size_t operator()(const tuple&amp;lt;int,int,int&amp;gt;&amp;amp; t) const {
size_t h = 17;
h = h * 53 + hash&amp;lt;int&amp;gt;{}(get&amp;lt;0&amp;gt;(t));
h = h * 53 + hash&amp;lt;int&amp;gt;{}(get&amp;lt;1&amp;gt;(t));
h = h * 53 + hash&amp;lt;int&amp;gt;{}(get&amp;lt;2&amp;gt;(t));
return h;
}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The constant 53 (a prime) spreads bits well and reduces collision clustering.&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 id="an-important-caveat-rehashing"&gt;An Important Caveat: Rehashing&lt;/h3&gt;
&lt;p&gt;When the load factor (number of elements / number of buckets) exceeds a threshold (default 1.0 in most implementations), the hash table &lt;strong&gt;rehashes&lt;/strong&gt;: it allocates a larger bucket array and reinserts every element. This is O(n) and can be surprising if it happens inside a hot loop.&lt;/p&gt;
&lt;p&gt;You can pre-allocate to avoid rehashing:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cpp"&gt;unordered_map&amp;lt;int, int&amp;gt; freq;
freq.reserve(1024); // pre-allocate buckets
freq.max_load_factor(0.25); // keep table sparse for faster lookup
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="set-and-map"&gt;&lt;code&gt;set&lt;/code&gt; and &lt;code&gt;map&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Both are implemented as a &lt;strong&gt;red-black tree&lt;/strong&gt; — a self-balancing binary search tree that guarantees O(log n) for all operations in the worst case, and maintains elements in sorted order.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Complexity:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Worst case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Insert&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lookup&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Min/Max&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-order traversal&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;When to use:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need elements in sorted order (e.g., iterating from smallest to largest).&lt;/li&gt;
&lt;li&gt;You need range queries: &lt;code&gt;lower_bound&lt;/code&gt;, &lt;code&gt;upper_bound&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You need stable worst-case guarantees (no hash collision spikes).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="range-queries"&gt;Range Queries&lt;/h3&gt;
&lt;p&gt;The tree structure enables efficient range operations not possible with hash containers:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cpp"&gt;map&amp;lt;int, string&amp;gt; m;
// ... populate ...
// All entries with keys in [lo, hi]
auto it = m.lower_bound(lo);
while (it != m.end() &amp;amp;&amp;amp; it-&amp;gt;first &amp;lt;= hi) {
cout &amp;lt;&amp;lt; it-&amp;gt;first &amp;lt;&amp;lt; &amp;quot; -&amp;gt; &amp;quot; &amp;lt;&amp;lt; it-&amp;gt;second &amp;lt;&amp;lt; &amp;quot;\n&amp;quot;;
++it;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="performance-pitfall-cache-misses"&gt;Performance Pitfall: Cache Misses&lt;/h3&gt;
&lt;p&gt;Red-black trees store nodes on the heap with pointer chasing. For large sets, this leads to many &lt;strong&gt;cache misses&lt;/strong&gt; compared to a contiguous structure like &lt;code&gt;vector&lt;/code&gt; or a hash table with a flat bucket array. For small n (&amp;lt; a few hundred elements), a sorted &lt;code&gt;vector&lt;/code&gt; with binary search often outperforms &lt;code&gt;set&lt;/code&gt; due to cache locality.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cpp"&gt;// For small, mostly-read sets, this can be faster:
vector&amp;lt;int&amp;gt; v = {3, 1, 4, 1, 5, 9};
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
bool found = binary_search(v.begin(), v.end(), 4); // O(log n), cache-friendly
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="vector-the-default-choice"&gt;&lt;code&gt;vector&lt;/code&gt;: The Default Choice&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;vector&lt;/code&gt; is a contiguous dynamic array. It is almost always the right choice unless you have a specific reason to use another container, because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cache-friendly&lt;/strong&gt;: elements are laid out sequentially in memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Random access in O(1)&lt;/strong&gt;: direct indexing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Append in amortized O(1)&lt;/strong&gt;: push_back doubles capacity on resize.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Complexity:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Complexity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Random access&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;push_back&lt;/td&gt;
&lt;td&gt;O(1) amortized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Insert/delete at middle&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search (unsorted)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search (sorted, binary)&lt;/td&gt;
&lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="avoiding-reallocations"&gt;Avoiding Reallocations&lt;/h3&gt;
&lt;p&gt;Like &lt;code&gt;unordered_map&lt;/code&gt;, &lt;code&gt;vector&lt;/code&gt; resizes when it runs out of capacity. Pre-allocate when you know the approximate size:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cpp"&gt;vector&amp;lt;int&amp;gt; v;
v.reserve(10000); // allocate once, avoid repeated reallocations
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;size()&lt;/code&gt; vs &lt;code&gt;capacity()&lt;/code&gt;: &lt;code&gt;size&lt;/code&gt; is the number of elements currently stored; &lt;code&gt;capacity&lt;/code&gt; is how much memory is allocated. After &lt;code&gt;reserve(n)&lt;/code&gt;, &lt;code&gt;size&lt;/code&gt; is unchanged but &lt;code&gt;capacity &amp;gt;= n&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="summary-which-container-to-pick"&gt;Summary: Which Container to Pick?&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Need key-value pairs?
├─ Yes → Need sorted keys or range queries?
│ ├─ Yes → map
│ └─ No → unordered_map (faster)
└─ No → Need uniqueness only?
├─ Yes → Need sorted order?
│ ├─ Yes → set
│ └─ No → unordered_set (faster)
└─ No → vector (default)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When performance matters, always &lt;strong&gt;measure&lt;/strong&gt;. The theoretical complexity advantage of O(1) hash lookup over O(log n) tree lookup may be dwarfed by cache effects, rehashing, or a bad hash function in practice.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;See &lt;a href="https://stackoverflow.com/questions/2634690/good-hash-function-for-a-2d-index/2634715#2634715" target="_blank" rel="noopener"&gt;this StackOverflow discussion&lt;/a&gt; for more on polynomial hashing for multi-dimensional indices.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item></channel></rss>