[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