[libcxx-commits] [libcxx] [libc++] fix minor performance issue in `basic_string<C>::append()` (PR #210078)
Pavel Novikov via libcxx-commits
libcxx-commits at lists.llvm.org
Tue Jul 21 05:49:21 PDT 2026
================
@@ -482,6 +483,41 @@ int main(int argc, char** argv) {
std::bind_front(bench_impl, std::integral_constant<size_t, large_size>{}, std::false_type{}));
}
+ {
+ static auto bench_impl =
+ []<size_t size, bool opaque, class CharT>(
+ std::integral_constant<size_t, size>,
+ std::bool_constant<opaque>,
+ std::type_identity<CharT>,
+ benchmark::State& state) {
+ std::basic_string<CharT> src(size, 'a');
+ auto getIterator = [&src](size_t i) {
+ // INT_MAX because we want overhead of ThrowingIterator without actually throwing
+ return ThrowingIterator<CharT>(src.data() + i, src.data() + src.size(), INT_MAX);
----------------
toughengineer wrote:
tl;dr: I don't know what's special about `ThrowingIterator`, my guess is that it brings enough overhead that we can measure a difference.
-----
I conducted an experiment.
I replaced iterators with `string`'s random access iterators, and predictably there is no difference (because a different part of code is actually at play):
```c++
benchmark::DoNotOptimize(string.append(getIterator(0), getIterator(size)));
```
Let it be for reference.
before (without changes):
```
2026-07-21T15:24:32+03:00
Running ./bench
Run on (20 X 3494.4 MHz CPU s)
CPU Caches:
L1 Data 48 KiB (x10)
L1 Instruction 32 KiB (x10)
L2 Unified 2048 KiB (x10)
L3 Unified 24576 KiB (x1)
Load Average: 0.12, 0.04, 0.02
***WARNING*** ASLR is enabled, the results may have unreproducible noise in them.
-------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------------
std::string::append(ForwardIt, ForwardIt) (opaque)/5 5.73 ns 5.73 ns 113543649
std::u8string::append(ForwardIt, ForwardIt) (opaque)/5 5.82 ns 5.82 ns 114649223
std::wstring::append(ForwardIt, ForwardIt) (opaque)/5 3.01 ns 3.01 ns 228943794
std::string::append(ForwardIt, ForwardIt) (opaque)/30 3.02 ns 3.02 ns 230250131
std::u8string::append(ForwardIt, ForwardIt) (opaque)/30 3.04 ns 3.04 ns 230234151
std::wstring::append(ForwardIt, ForwardIt) (opaque)/30 3.64 ns 3.64 ns 192984729
std::string::append(ForwardIt, ForwardIt) (transparent)/5 6.13 ns 6.13 ns 112138334
std::u8string::append(ForwardIt, ForwardIt) (transparent)/5 6.22 ns 6.22 ns 108915852
std::wstring::append(ForwardIt, ForwardIt) (transparent)/5 3.04 ns 3.04 ns 232011948
std::string::append(ForwardIt, ForwardIt) (transparent)/30 3.02 ns 3.02 ns 231529793
std::u8string::append(ForwardIt, ForwardIt) (transparent)/30 3.04 ns 3.04 ns 232243723
std::wstring::append(ForwardIt, ForwardIt) (transparent)/30 3.64 ns 3.64 ns 193481498
```
after (with changes):
```
2026-07-21T15:24:55+03:00
Running ./bench
Run on (20 X 3494.4 MHz CPU s)
CPU Caches:
L1 Data 48 KiB (x10)
L1 Instruction 32 KiB (x10)
L2 Unified 2048 KiB (x10)
L3 Unified 24576 KiB (x1)
Load Average: 0.30, 0.09, 0.04
***WARNING*** ASLR is enabled, the results may have unreproducible noise in them.
-------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------------
std::string::append(ForwardIt, ForwardIt) (opaque)/5 5.78 ns 5.78 ns 123796740
std::u8string::append(ForwardIt, ForwardIt) (opaque)/5 5.72 ns 5.72 ns 118276009
std::wstring::append(ForwardIt, ForwardIt) (opaque)/5 3.04 ns 3.04 ns 228957048
std::string::append(ForwardIt, ForwardIt) (opaque)/30 3.03 ns 3.03 ns 228371878
std::u8string::append(ForwardIt, ForwardIt) (opaque)/30 3.03 ns 3.03 ns 229643272
std::wstring::append(ForwardIt, ForwardIt) (opaque)/30 3.66 ns 3.66 ns 192805491
std::string::append(ForwardIt, ForwardIt) (transparent)/5 5.98 ns 5.98 ns 113876132
std::u8string::append(ForwardIt, ForwardIt) (transparent)/5 6.14 ns 6.14 ns 110579277
std::wstring::append(ForwardIt, ForwardIt) (transparent)/5 3.03 ns 3.03 ns 233406623
std::string::append(ForwardIt, ForwardIt) (transparent)/30 3.03 ns 3.03 ns 232780093
std::u8string::append(ForwardIt, ForwardIt) (transparent)/30 3.05 ns 3.05 ns 230290656
std::wstring::append(ForwardIt, ForwardIt) (transparent)/30 3.64 ns 3.64 ns 192181138
```
-----
Then I commented `throw`s in the `ThrowingIterator`'s implementation, the benchmark code looks like this:
```c++
auto getIterator = [&src](size_t i) {
// INT_MAX because we want overhead of ThrowingIterator without actually throwing
return ThrowingIterator<CharT>(src.data() + i, src.data() + src.size(), INT_MAX);
};
benchmark::DoNotOptimize(string.append(getIterator(0), getIterator(size)));
```
It gave no noticeable difference. It is noticeably slower than random access iterators though.
before (without changes):
```
2026-07-21T15:25:56+03:00
Running ./bench
Run on (20 X 3494.4 MHz CPU s)
CPU Caches:
L1 Data 48 KiB (x10)
L1 Instruction 32 KiB (x10)
L2 Unified 2048 KiB (x10)
L3 Unified 24576 KiB (x1)
Load Average: 0.20, 0.11, 0.05
***WARNING*** ASLR is enabled, the results may have unreproducible noise in them.
-------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------------
std::string::append(ForwardIt, ForwardIt) (opaque)/5 8.91 ns 8.91 ns 71140091
std::u8string::append(ForwardIt, ForwardIt) (opaque)/5 8.86 ns 8.86 ns 77177168
std::wstring::append(ForwardIt, ForwardIt) (opaque)/5 14.2 ns 14.2 ns 48254297
std::string::append(ForwardIt, ForwardIt) (opaque)/30 38.9 ns 38.9 ns 18012853
std::u8string::append(ForwardIt, ForwardIt) (opaque)/30 39.4 ns 39.4 ns 17823351
std::wstring::append(ForwardIt, ForwardIt) (opaque)/30 39.1 ns 39.1 ns 18006097
std::string::append(ForwardIt, ForwardIt) (transparent)/5 8.92 ns 8.92 ns 76522971
std::u8string::append(ForwardIt, ForwardIt) (transparent)/5 9.00 ns 9.00 ns 74710896
std::wstring::append(ForwardIt, ForwardIt) (transparent)/5 14.7 ns 14.7 ns 47397890
std::string::append(ForwardIt, ForwardIt) (transparent)/30 39.1 ns 39.1 ns 17958756
std::u8string::append(ForwardIt, ForwardIt) (transparent)/30 39.4 ns 39.4 ns 17684654
std::wstring::append(ForwardIt, ForwardIt) (transparent)/30 38.9 ns 38.9 ns 18021868
```
after (with changes):
```
2026-07-21T15:26:19+03:00
Running ./bench
Run on (20 X 3494.4 MHz CPU s)
CPU Caches:
L1 Data 48 KiB (x10)
L1 Instruction 32 KiB (x10)
L2 Unified 2048 KiB (x10)
L3 Unified 24576 KiB (x1)
Load Average: 0.35, 0.15, 0.06
***WARNING*** ASLR is enabled, the results may have unreproducible noise in them.
-------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------------
std::string::append(ForwardIt, ForwardIt) (opaque)/5 8.88 ns 8.88 ns 76256794
std::u8string::append(ForwardIt, ForwardIt) (opaque)/5 8.84 ns 8.84 ns 78165828
std::wstring::append(ForwardIt, ForwardIt) (opaque)/5 14.9 ns 14.9 ns 46379577
std::string::append(ForwardIt, ForwardIt) (opaque)/30 39.9 ns 39.9 ns 17357680
std::u8string::append(ForwardIt, ForwardIt) (opaque)/30 39.0 ns 39.0 ns 18012334
std::wstring::append(ForwardIt, ForwardIt) (opaque)/30 40.0 ns 40.0 ns 17735141
std::string::append(ForwardIt, ForwardIt) (transparent)/5 9.14 ns 9.14 ns 74071017
std::u8string::append(ForwardIt, ForwardIt) (transparent)/5 9.14 ns 9.14 ns 75885036
std::wstring::append(ForwardIt, ForwardIt) (transparent)/5 16.2 ns 16.2 ns 41903270
std::string::append(ForwardIt, ForwardIt) (transparent)/30 40.2 ns 40.2 ns 16823389
std::u8string::append(ForwardIt, ForwardIt) (transparent)/30 40.7 ns 40.7 ns 16924884
std::wstring::append(ForwardIt, ForwardIt) (transparent)/30 40.4 ns 40.4 ns 16972318
```
I guess even with the bookkeeping the overhead is small enough to not result in significant performance hit.
-----
Then I used `ThrowingIterator` as-is (with `throw`s uncommented), the benchmark code looks the same as in previous case:
```c++
benchmark::DoNotOptimize(string.append(getIterator(0), getIterator(size)));
```
Here it's even slower than the previos "forward iterator" case, and I can see noticeable difference between the versions of `append()` implementation.
My understanding is that one way or another `throw` adds enough overhead that it starts to be noticeable (compare 9-ish ns without `throw` and 20-ish ns with it).
before (without changes):
```
2026-07-21T15:27:28+03:00
Running ./bench
Run on (20 X 3494.4 MHz CPU s)
CPU Caches:
L1 Data 48 KiB (x10)
L1 Instruction 32 KiB (x10)
L2 Unified 2048 KiB (x10)
L3 Unified 24576 KiB (x1)
Load Average: 0.20, 0.16, 0.07
***WARNING*** ASLR is enabled, the results may have unreproducible noise in them.
-------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------------
std::string::append(ForwardIt, ForwardIt) (opaque)/5 26.3 ns 26.3 ns 26660035
std::u8string::append(ForwardIt, ForwardIt) (opaque)/5 26.7 ns 26.7 ns 26714356
std::wstring::append(ForwardIt, ForwardIt) (opaque)/5 31.6 ns 31.6 ns 21982957
std::string::append(ForwardIt, ForwardIt) (opaque)/30 118 ns 118 ns 5840702
std::u8string::append(ForwardIt, ForwardIt) (opaque)/30 108 ns 108 ns 6089127
std::wstring::append(ForwardIt, ForwardIt) (opaque)/30 111 ns 111 ns 6104910
std::string::append(ForwardIt, ForwardIt) (transparent)/5 26.8 ns 26.8 ns 26490989
std::u8string::append(ForwardIt, ForwardIt) (transparent)/5 27.8 ns 27.8 ns 25610574
std::wstring::append(ForwardIt, ForwardIt) (transparent)/5 31.8 ns 31.8 ns 21787801
std::string::append(ForwardIt, ForwardIt) (transparent)/30 116 ns 116 ns 5976465
std::u8string::append(ForwardIt, ForwardIt) (transparent)/30 110 ns 110 ns 6170587
std::wstring::append(ForwardIt, ForwardIt) (transparent)/30 109 ns 109 ns 6310451
```
after (with changes):
```
2026-07-21T15:27:51+03:00
Running ./bench
Run on (20 X 3494.4 MHz CPU s)
CPU Caches:
L1 Data 48 KiB (x10)
L1 Instruction 32 KiB (x10)
L2 Unified 2048 KiB (x10)
L3 Unified 24576 KiB (x1)
Load Average: 0.27, 0.18, 0.08
***WARNING*** ASLR is enabled, the results may have unreproducible noise in them.
-------------------------------------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------------
std::string::append(ForwardIt, ForwardIt) (opaque)/5 19.1 ns 19.1 ns 36931421
std::u8string::append(ForwardIt, ForwardIt) (opaque)/5 19.2 ns 19.2 ns 35455048
std::wstring::append(ForwardIt, ForwardIt) (opaque)/5 26.9 ns 26.9 ns 26051882
std::string::append(ForwardIt, ForwardIt) (opaque)/30 80.0 ns 80.0 ns 8576529
std::u8string::append(ForwardIt, ForwardIt) (opaque)/30 76.9 ns 76.9 ns 8798004
std::wstring::append(ForwardIt, ForwardIt) (opaque)/30 76.1 ns 76.1 ns 8991720
std::string::append(ForwardIt, ForwardIt) (transparent)/5 18.9 ns 18.9 ns 37472571
std::u8string::append(ForwardIt, ForwardIt) (transparent)/5 18.9 ns 18.9 ns 36373991
std::wstring::append(ForwardIt, ForwardIt) (transparent)/5 25.5 ns 25.5 ns 26504911
std::string::append(ForwardIt, ForwardIt) (transparent)/30 79.8 ns 79.8 ns 8700310
std::u8string::append(ForwardIt, ForwardIt) (transparent)/30 77.8 ns 77.8 ns 9002752
std::wstring::append(ForwardIt, ForwardIt) (transparent)/30 77.3 ns 77.3 ns 8921482
```
I guess one could add overhead to iterator increment/decrement little by little and see when the difference starts to be noticeable.
https://github.com/llvm/llvm-project/pull/210078
More information about the libcxx-commits
mailing list