[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