August 18th, 2024

`noexcept` affects libstdc++'s `unordered_set`

The article examines the impact of the `noexcept` specifier on `std::unordered_set` performance in libstdc++, highlighting optimization opportunities and advocating for improvements to handle hash function efficiency better.

Read original articleLink Icon
`noexcept` affects libstdc++'s `unordered_set`

The article discusses how the `noexcept` specifier impacts the performance of `std::unordered_set` in GNU libstdc++. It explains that the layout of nodes in hash-based associative containers can change based on whether the hash function is marked as `noexcept`. The author illustrates how the `erase` function can be optimized by precomputing and storing the hash value in each node, which can improve performance during element removal and rehashing. The article highlights that libstdc++ does not cache hash values by default, assuming that recomputing them is inexpensive, which can lead to performance issues when using non-trivial hash functions. The author provides benchmarks showing the performance differences based on the type of hash function used, emphasizing that marking a non-trivial hash function as `noexcept` can lead to increased calls to the hash function and slower performance. The conclusion suggests that while these details may not be critical for most programming tasks, they highlight the importance of understanding how `noexcept` can affect performance in specific scenarios. The author advocates for improvements in libstdc++ to better handle these cases.

- The `noexcept` specifier can significantly affect the performance of `std::unordered_set`.

- Storing hash values in nodes can optimize performance during element removal and rehashing.

- libstdc++ assumes recomputing hash values is cheap, which can lead to inefficiencies with non-trivial hash functions.

- Marking non-trivial hash functions as `noexcept` can result in performance degradation.

- The author calls for improvements in libstdc++ to address these performance issues.

Link Icon 5 comments
By @z_open - 4 months
somewhat unrelated but it's worth pointing out that noexcept is more specific for move semantics.

In fact most c++ developers believe that throwing an exception in a noexcept function is undefined behavior. It is not. The behavior is defined to call std::terminate. Which would lead one to ask how does it know to call that. Because noexcept functions have a hidden try catch to see if it should call it. The result is that noexcept can hurt performance, which is surprising behavior. C++ is just complicated.

By @formerly_proven - 4 months
Money quote

> The outcome is that libstdc++’s unordered_set has performance characteristics that subtly depend on the true name and noexceptness of the hash function.

> - A user-defined struct H : std::hash<std::string> {} will see smaller allocations and more calls to the hash function, than if you had just used std::hash<std::string> directly. (Because std::hash<std::string> is on the blacklist and H is not.)

> - A user-defined hasher with size_t operator() const noexcept will see smaller allocations and more calls to the hash function (especially during rehashing). One with size_t operator() const will see larger allocations and fewer calls to the hash function.

By @leni536 - 4 months
Also, I hope that if you’re reading this post in a year or two (say, December 2025), these specific examples won’t even reproduce anymore. I hope libstdc++ gets on the ball and eliminates some of this weirdness. (In short: They should get rid of the blacklist; pay the 8 bytes by default; introduce a whitelist for trivial hashers specifically; stop checking noexceptness for any reason.)

I think this would be an ABI break.

By @quotemstr - 4 months
If you care about performance, you should consider using Abseil's hash tables instead of unordered_set and unordered_map. The Abseil containers are mostly drop in replacements for the standard unordered containers, except they have a tweaked API (e.g. erase() returning void) that admits important performance optimizations and (except the node based ones) use open addressed hash tables instead of chaining (i.e. each hash bucket is no longer a linked list). You end up with 2x-3x speedups in a lot of scenarios. The standard containers can't be fixed to match the performance of the Abseil ones because 1) the specification API requires pessimization, 2) Linux distributors are reluctant to break C++ "ABI" (such as it is).

I mean, of course you should follow this article's advice on noexcept, but there's a whole world of associative container performance optimizations out there.

By @mgaunard - 4 months
if you want a fast hash map, just use a library that is built specifically around a particular implementation strategy.

std unordered map is slow by design for several reasons.

Things of interest are:

- open addressing method: linear, quadratic, robinhood, hybrid

- the default hash function

- the logic for the number of buckets (power of two, prime) and associated hash projection (fibonacci, modulo).

Boost seems to be doing a decent job at this now.