Micro-benchmarking Type Erasure: std::function vs. Abseil vs. Boost vs. Function2 (Clang 20, Ryzen 9 9950X)
I'm currently developing SereneDB and some time ago we performed some micro-benchmarks to evaluate the call overhead of `std::function` against popular alternatives.
We compared
* `std::function`
* `absl::AnyInvocable`, `absl::FunctionRef`
* `boost::function`
* `fu2::function` / `fu2::unique_function`
Setup
* **CPU:** AMD Ryzen 9 9950X 16-Core (Zen 5)
* **Compiler:** Clang 20.1.8 (-O3)
* **Std Lib:** libc++ 20 (ABI v2)
* **Methodology:** Follows Abseil's micro-benchmarking practices (using DoNotOptimize to prevent dead-code elimination).
* Benchmark source code is available [**here**](https://github.com/serenedb/serenedb/blob/main/tests/bench/micro/function.cpp)**.**
Results and notes ([click here to see the visualized results](https://imgur.com/JUX738l))
|**Trivial Lambda**|||
|:-|:-|:-|
|`std::function`|**0.91 ns**|Surprisingly fast, likely because libc++ is devirtualizing this|
|`absl::FunctionRef`|0.90 ns|Non-owning, consistently fast|
|`boost::function`|0.95 ns||
|`absl::AnyInvocable`|1.81 ns||
|`fu2::function`|4.77 ns|Significant overhead (likely missed devirtualization)|
|**Large Lambda (SBO Check)**|||
|`std::function`|5.51 ns|Hit the allocation|
|`absl::FunctionRef`|**1.09 ns**|Immune to capture size (reference semantics)|
|`boost::function`|10.20 ns|Heaviest penalty for large captures|
|`fu2::function`|6.06 ns||
|**Function Pointers**|||
|`absl::FunctionRef`|1.08 ns||
|`absl::FunctionValue`|**0.89 ns**||
|`std::function`|1.10 ns||
|`fu2::function_view`|1.09 ns|The view variant performs well|
|**With Non-Trivial Args**|||
|absl::FunctionRef|2.53 ns|Slightly slower than std::function here|
|`std::function`|**2.39 ns**||
|`absl::AnyInvocable`|2.39 ns||
|`boost::function`|3.84 ns||
# Key Observations
1. **Clang & libc++:** The most surprising result is `std::function` (0.91ns) beating `absl::AnyInvocable` and `fu2` in the trivial case. Since we're using Clang 20 with libc++, the compiler is likely seeing through the type erasure and devirtualizing the call completely.
2. **Views are great:** If you don't need ownership, `absl::FunctionRef` (or `fu2::function_view`) beats owning wrappers in performance. `absl::FunctionRef` remained \~1ns even when the underlying lambda was large, whereas `std::function` jumped to \~5.5ns due to allocation/SBO limits.
3. **The function2 (fu2) poor results:** We observed `fu2::function` hovering around \~4.8ns for trivial cases. Since `std::function` is <1ns, this suggests that while Clang could inline the standard library implementation, it failed to devirtualize the `fu2` vtable, resulting in a true indirect call.
4. **Features vs Raw Speed:** While `fu2` lagged in this specific micro-benchmark, it provides powerful features that `std::function` lacks, such as function overloading.
5. **Boost:** Shows its age slightly with the highest penalty for large captures (10.2ns).
# Conclusion
Based on the results, at **SereneDB** we decided to stick to `std::function` or `absl::FunctionRef` depending on the use case (ownership vs. non-ownership), as they currently offer the best performance-to-complexity ratio for our specific compiler setup.
repo: [https://github.com/serenedb/serenedb](https://github.com/serenedb/serenedb)
https://redd.it/1puz450
@r_cpp
Post #24568
22