[libcxx-commits] [libcxx] [libc++] Treat negative counts in copy_n & friends as no-ops (PR #207086)
Louis Dionne via libcxx-commits
libcxx-commits at lists.llvm.org
Thu Jul 2 09:21:31 PDT 2026
================
@@ -63,14 +63,15 @@ _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _OutputIterator
copy_n(_InputIterator __first, _Size __n, _OutputIterator __result) {
using _IntegralSize = decltype(std::__convert_to_integral(__n));
_IntegralSize __converted = __n;
- if (__converted > 0) {
+ if (__converted < 0) [[__unlikely__]]
----------------
ldionne wrote:
It seems to make a minor difference: https://godbolt.org/z/dxhGzT6ME.
I used Claude to help me analyze the diff and it seems to mostly do two things:
1. For `copy_n` for input iterators, it moves the cold block to the tail of the function. Possible hot/cold improvement here for the common codepath.
2. For `fill_n(vector<bool>)`, it actually ends up removing a "shrink wrapping" optimization. I didn't know about this optimization and IDK if you do, so let me paste this here:
> Here the function has a heavy prologue: 5 stp pairs spilling 10 callee-saved registers (x19–x26, x29/x30), because the body calls memset/bzero and needs lots of live registers.
>
> Without [[unlikely]] — LLVM shrink-wraps: it hoists the n <= 0 test above the prologue:
>
> cmp x1, #0
> b.le LBB4_4 ; n <= 0: bail out before saving anything
> stp x26, x25, [sp, #-80]! ; prologue only runs when n > 0
> ...
> LBB4_4: ; cheap early return — no reg restores, no sp adjust
> ldr x9, [x0]; str x9, [x8]; ldr w9, [x0,# 8]; str w9, [x8,# 8]
> ret
>
> With [[unlikely]] — the shrink-wrap cost model sees the n <= 0 exit as cold, decides it's not worth it, and emits the full prologue first, then the check:
>
> stp x26, x25, [sp, #-80]! ; save all 10 regs up front
> ... (4 more stp) ...
> add x29, sp, #64
> mov x21, x0
> mov x19, x8
> cmp x1, #0
> b.le LBB4_19 ; and this cold exit now has to *undo* the prologue:
> LBB4_19:
> ldr x8,[x21]; str x8,[x19]; ldr w8,[x21,# 8]
> b LBB4_18 ; -> ldp x29/x30, x20/x19, x22/x21, x24/x23, x26/x25; ret
>
> The knock-on effect is slightly different register allocation in the body (a few extra/rearranged movs saving values across the memset/bzero calls), because the two CFG shapes give the allocator different freedom.
Interestingly, we actually seem to pay more cost in the cold case for `fill_n`.
Bottom line: I'd definitely remove it in `fill_n(vector<bool>)`, and I'm neutral about removing it from the other places.
https://github.com/llvm/llvm-project/pull/207086
More information about the libcxx-commits
mailing list