Skip to content

Investigate GCC 13 bench gap versus Clang #1

Description

@equationzhao

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:

  1. 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.
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions