Background
CI release-bench now runs the Boost comparison on five jobs (commit 56fb5af, run 31680043512):
linux-x64-gcc13 — GCC 13.3, libstdc++
linux-x64-clang-libstdcxx — Clang 18, libstdc++
linux-x64-clang-libcxx — Clang 18, libc++
linux-arm64-gcc13 — GCC 13.3, libstdc++
macos-arm64-clang — Apple Clang 21, libc++
All five banners report n=1000000 cap=4096 boost=on best-of-5 alloc_empty=yes. Ratio is eqz ns/op ÷ Boost ns/op (< 1 means eqz is faster). Shared runners are noisy; this issue records the split so we do not retune Clang-tuned hot paths from a single GCC artifact.
What the matrix shows
The gap is the compiler, not the standard library. Same Ubuntu x64 runner, same libstdc++:
| Job |
compiler |
stdlib |
geomean |
wins |
hot/slide |
scan |
structural |
| macos-arm64-clang |
clang+21 |
libc++ |
0.567 |
24/32 |
0.731 (12/20) |
0.523 (7/7) |
0.229 (5/5) |
| linux-x64-clang-libcxx |
clang+18 |
libc++ |
0.838 |
20/32 |
0.952 (12/20) |
0.488 (6/7) |
1.07 (2/5) |
| linux-x64-clang-libstdcxx |
clang+18 |
libstdc++ |
0.859 |
21/32 |
0.993 (12/20) |
0.479 (7/7) |
1.09 (2/5) |
| linux-x64-gcc13 |
gcc+13.3 |
libstdc++ |
1.290 |
13/32 |
1.512 (7/20) |
1.527 (2/7) |
0.540 (4/5) |
| linux-arm64-gcc13 |
gcc+13.3 |
libstdc++ |
1.372 |
11/32 |
1.797 (5/20) |
1.251 (2/7) |
0.531 (4/5) |
Clang libc++ (0.838) vs Clang libstdc++ (0.859) is a wash. Switching the same libstdc++ from Clang 18 to GCC 13 moves geomean from 0.86 to 1.29.
macOS Apple Clang still matches local Mac numbers (~0.57, 24/32, scan 7/7).
GCC hot path (do not chase from one run)
On Linux x64, Clang stays near Boost on the overwrite/slide cases that GCC loses:
| Scenario |
GCC 13 x64 |
Clang 18 libstdc++ |
Apple Clang 21 |
slide/u64/4097 |
4.378 |
0.977 |
0.969 |
burst/clear_refill |
4.019 |
1.000 |
1.010 |
fill/not_full |
4.015 |
1.000 |
1.011 |
slide/push_front |
3.682 |
0.959 |
0.988 |
slide/u64/cap2 |
2.829 |
1.131 |
0.977 |
slide/string_sso |
2.240 |
0.999 |
1.002 |
emptyish/size1 |
2.330 |
0.618 |
0.240 |
slide/string_heap |
1.946 |
1.588 |
1.037 |
Absolute times for slide/u64/4097 on Linux x64: eqz 5.02 ns vs Boost 1.15 ns (GCC); eqz 1.02 ns vs Boost 1.05 ns (Clang libstdc++). eqz is ~5× slower under GCC, not a few percent.
GCC Boost itself is bimodal: slide/u64/4096 Boost is 6.67 ns while slide/u64/4097 Boost is 1.15 ns. eqz is ~5 ns on both, so the 4096 0.74 WIN is Boost being slow, not eqz being fast. Do not retune bump / drop / wrap from that ratio.
Tricks already measured worse or wrong on the Clang suite (keep out unless a GCC-only godbolt says otherwise): dedicated push_back duplicating emplace, skip ensure_writable when cap > 0, in-place ++last_ duplicating bump, inlining the cap-0 throw into emplace, power-of-two bitmask, Boost-style T* linear iterators.
copy/full goes the other way — and is unstable
| Platform |
eqz ns/op |
Boost ns/op |
ratio |
| macOS arm64 Clang 21 |
602 |
4087 |
0.147 |
| Linux x64 GCC 13 |
814 |
1589 |
0.513 |
| Linux arm64 GCC 13 |
420 |
1257 |
0.334 |
| Linux x64 Clang 18 libc++ |
11443 |
3865 |
2.961 |
| Linux x64 Clang 18 libstdc++ |
11479 |
3864 |
2.971 |
400–800 ns for 4096 uint64_t looks like memcpy. 11k+ ns looks like element-wise construct. Both Linux Clang stdlibs are slow, so this is the compiler, not allocator/STL. All banners say alloc_empty=yes, so the empty-allocator gate is not the difference.
Do not treat this GCC 0.51× as proof that trivial_memcpy is on. A previous Ubuntu GCC 13 run on the same bench had copy/full at 6.9× (eqz ~12765 ns). Runner noise is large enough to flip the memcpy story. Re-measure before changing the copy path.
Linux Clang copy/* (~3×) is the one stable loss in an otherwise Clang-winning matrix. That is the first copy-path question: does if constexpr (trivial_memcpy) plus !is_constant_evaluated() actually take the memcpy branch on Clang 18 -O2/-O3?
Full ratio table (eqz / Boost)
| Scenario |
macOS Clang 21 |
Clang libc++ |
Clang libstdc++ |
GCC 13 x64 |
GCC 13 arm64 |
| slide/u64/4096 |
1.103 |
0.989 |
0.996 |
0.740 |
1.109 |
| slide/u64/cap1 |
0.826 |
1.105 |
1.104 |
0.564 |
1.091 |
| slide/u64/cap3 |
1.022 |
1.027 |
1.021 |
0.619 |
1.113 |
| slide/u64/1M |
0.986 |
0.981 |
0.978 |
1.167 |
0.716 |
| push_only/full |
0.979 |
1.002 |
1.000 |
0.796 |
0.954 |
| fill/not_full |
1.011 |
1.039 |
1.000 |
4.015 |
4.258 |
| queue/write_read |
0.966 |
0.858 |
0.864 |
0.767 |
0.890 |
| slide/push_front |
0.988 |
0.974 |
0.959 |
3.682 |
1.991 |
| mixed/ends |
0.791 |
1.006 |
0.970 |
0.898 |
0.949 |
| iterate/wrapped |
0.327 |
0.357 |
0.362 |
1.923 |
1.906 |
| iterate/linear |
0.356 |
0.334 |
0.298 |
1.995 |
1.922 |
| index/seq |
0.711 |
0.502 |
0.503 |
2.122 |
1.138 |
| index/rand |
0.939 |
0.899 |
0.874 |
0.976 |
0.947 |
| slide/cacheline |
0.990 |
0.948 |
0.906 |
0.486 |
0.998 |
| slide/string_sso |
1.002 |
0.839 |
0.999 |
2.240 |
1.794 |
| slide/string_heap |
1.037 |
0.957 |
1.588 |
1.946 |
1.375 |
| copy/full |
0.147 |
2.961 |
2.971 |
0.513 |
0.334 |
| insert/middle |
0.102 |
0.167 |
0.156 |
0.268 |
0.280 |
| linearize+scan |
0.278 |
0.952 |
1.122 |
0.798 |
0.266 |
| slide/u64/cap2 |
0.977 |
1.088 |
1.131 |
2.829 |
2.219 |
| slide/u64/4097 |
0.969 |
1.000 |
0.977 |
4.378 |
2.003 |
| queue/steady_half |
1.039 |
0.871 |
0.992 |
1.313 |
2.274 |
| drain/pop_front |
1.029 |
0.951 |
0.973 |
1.643 |
3.200 |
| stack/push_pop_back |
0.995 |
1.013 |
1.008 |
1.503 |
5.482 |
| emptyish/size1 |
0.240 |
0.632 |
0.618 |
2.330 |
1.725 |
| burst/clear_refill |
1.010 |
0.868 |
1.000 |
4.019 |
4.053 |
| index/wrapped |
0.720 |
0.506 |
0.506 |
2.023 |
1.516 |
| iterate/reverse |
0.204 |
0.240 |
0.242 |
1.632 |
0.784 |
| array_scan/wrapped |
0.934 |
1.005 |
0.994 |
0.736 |
1.021 |
| copy/wrapped |
0.161 |
2.977 |
2.955 |
0.279 |
0.274 |
| move/ctor |
0.929 |
1.000 |
0.998 |
1.497 |
6.183 |
| try_push/full |
0.011 |
1.018 |
1.026 |
1.990 |
6.105 |
try_push/full on macOS (0.011) is a timer floor (eqz prints 0.00 ns/op). Ignore it.
Artifacts: bench-<job>/bench.txt on that run. Local fetch: ./scripts/dev.sh fetch-ci-bench.
Objective
Understand and, if cheap, close the two compiler-specific holes without regressing Apple Clang / Linux Clang hot paths:
- Linux Clang 18
copy/full and copy/wrapped (~3×). Confirm whether trivial_memcpy is taken. If not, why (is_constant_evaluated, if constexpr, inlining). Fix only if it does not hurt GCC/Apple Clang copy.
- GCC 13 hot/slide (4097, fill, burst, push_front). Godbolt/objdump
emplace_back overwrite vs Boost on GCC 13 -O2. Prefer explaining Boost's 4096 vs 4097 bimodal over adding Clang-discarded micro-opts.
- Re-run GCC
copy/full several times (CI or ./scripts/dev.sh bench-gcc) before treating memcpy as on or off.
Out of scope unless the above is done: retuning wrap/bump/drop from Ubuntu ratio noise; adding a dedicated push_back; skipping ensure_writable when capacity() > 0; inlining the cap-0 throw.
Acceptance
- Short write-up (comment here or DESIGN.md) of what Clang 18 and GCC 13 actually emit for copy and for full
emplace_back.
- Any code change must keep macOS Clang geomean in the same band (~0.57, scan still 7/7) and must not make Linux Clang hot/slide worse than ~1.0.
- GCC geomean is not a merge gate. A confirmed memcpy miss or a GCC-only codegen bug with a small, local fix is enough.
Background
CI
release-benchnow runs the Boost comparison on five jobs (commit56fb5af, run 31680043512):linux-x64-gcc13— GCC 13.3, libstdc++linux-x64-clang-libstdcxx— Clang 18, libstdc++linux-x64-clang-libcxx— Clang 18, libc++linux-arm64-gcc13— GCC 13.3, libstdc++macos-arm64-clang— Apple Clang 21, libc++All five banners report
n=1000000 cap=4096 boost=on best-of-5 alloc_empty=yes. Ratio iseqz ns/op ÷ Boost ns/op(< 1means eqz is faster). Shared runners are noisy; this issue records the split so we do not retune Clang-tuned hot paths from a single GCC artifact.What the matrix shows
The gap is the compiler, not the standard library. Same Ubuntu x64 runner, same libstdc++:
Clang libc++ (0.838) vs Clang libstdc++ (0.859) is a wash. Switching the same libstdc++ from Clang 18 to GCC 13 moves geomean from 0.86 to 1.29.
macOS Apple Clang still matches local Mac numbers (~0.57, 24/32, scan 7/7).
GCC hot path (do not chase from one run)
On Linux x64, Clang stays near Boost on the overwrite/slide cases that GCC loses:
slide/u64/4097burst/clear_refillfill/not_fullslide/push_frontslide/u64/cap2slide/string_ssoemptyish/size1slide/string_heapAbsolute times for
slide/u64/4097on Linux x64: eqz 5.02 ns vs Boost 1.15 ns (GCC); eqz 1.02 ns vs Boost 1.05 ns (Clang libstdc++). eqz is ~5× slower under GCC, not a few percent.GCC Boost itself is bimodal:
slide/u64/4096Boost is 6.67 ns whileslide/u64/4097Boost is 1.15 ns. eqz is ~5 ns on both, so the 4096 0.74 WIN is Boost being slow, not eqz being fast. Do not retunebump/drop/ wrap from that ratio.Tricks already measured worse or wrong on the Clang suite (keep out unless a GCC-only godbolt says otherwise): dedicated
push_backduplicatingemplace, skipensure_writablewhencap > 0, in-place++last_duplicatingbump, inlining the cap-0 throw intoemplace, power-of-two bitmask, Boost-styleT*linear iterators.copy/fullgoes the other way — and is unstable400–800 ns for 4096
uint64_tlooks likememcpy. 11k+ ns looks like element-wise construct. Both Linux Clang stdlibs are slow, so this is the compiler, not allocator/STL. All banners sayalloc_empty=yes, so the empty-allocator gate is not the difference.Do not treat this GCC 0.51× as proof that
trivial_memcpyis on. A previous Ubuntu GCC 13 run on the same bench hadcopy/fullat 6.9× (eqz ~12765 ns). Runner noise is large enough to flip the memcpy story. Re-measure before changing the copy path.Linux Clang
copy/*(~3×) is the one stable loss in an otherwise Clang-winning matrix. That is the first copy-path question: doesif constexpr (trivial_memcpy)plus!is_constant_evaluated()actually take thememcpybranch on Clang 18-O2/-O3?Full ratio table (eqz / Boost)
try_push/fullon macOS (0.011) is a timer floor (eqz prints0.00 ns/op). Ignore it.Artifacts:
bench-<job>/bench.txton that run. Local fetch:./scripts/dev.sh fetch-ci-bench.Objective
Understand and, if cheap, close the two compiler-specific holes without regressing Apple Clang / Linux Clang hot paths:
copy/fullandcopy/wrapped(~3×). Confirm whethertrivial_memcpyis taken. If not, why (is_constant_evaluated,if constexpr, inlining). Fix only if it does not hurt GCC/Apple Clang copy.emplace_backoverwrite vs Boost on GCC 13-O2. Prefer explaining Boost's 4096 vs 4097 bimodal over adding Clang-discarded micro-opts.copy/fullseveral times (CI or./scripts/dev.sh bench-gcc) before treating memcpy as on or off.Out of scope unless the above is done: retuning wrap/
bump/dropfrom Ubuntu ratio noise; adding a dedicatedpush_back; skippingensure_writablewhencapacity() > 0; inlining the cap-0 throw.Acceptance
emplace_back.