https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126678
Bug ID: 126678
Summary: Defaulted operator== on an iota_view value type makes
the enclosing loop be estimated ~7x colder than an
identical hand-written operator==
Product: gcc
Version: 16.1.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: c++
Assignee: unassigned at gcc dot gnu.org
Reporter: philodej at gmail dot com
Target Milestone: ---
Given a single-member "strong index" type used as the value type of a
std::ranges::iota_view, replacing a hand-written
bool operator==(const Idx& r) const noexcept { return v == r.v; }
with the equivalent
bool operator==(const Idx&) const noexcept = default;
makes GCC estimate the loop that iterates that view roughly 7x colder,
which then degrades code generation in the loop body. The two forms are
semantically identical and produce identical code for the operator itself
(it is inlined away in the early inliner either way), so the difference is
entirely in downstream analysis of the caller.
Idx::operator== is what terminates the iota_view loop, so it sits on the
loop's back edge.
Observed at -O3 (see the reproducer for exact flags and expected output):
- -fopt-info-inline-all reports the callee being inlined into the loop
as the same clone at the same size in both variants, but with a ~7x
difference in estimated time.
- As a consequence the register allocator treats the defaulted variant's
loop as cold: it keeps loop-carried values in caller-saved registers
and spills/reloads five of them around a call inside the loop, where
the hand-written variant promotes them to callee-saved registers and
spills only one.
- -fopt-info-loop-all additionally attributes the loop to a different
source location in the two variants -- the loop itself for the
hand-written form, but the operator== line for the defaulted one.
This one shows at -O2 as well and does not depend on any inlining
parameter, which suggests the loop's profile / trip-count information
is being lost rather than the cost model merely disagreeing.
The last point is probably the most useful starting place: it looks like a
profile/location attribution problem rather than an inliner tuning issue.
Compiler Explorer: https://godbolt.org/z/4EKjWzT7q
The difference is in the register-pressure handling around the Calc::update
call inside the loop:
explicit (time 260.9) — spills only the accumulator:
asm
.L17: movsd QWORD PTR [rsp+8], xmm2 ; save accumulator
call "Calc::update(double, double)"
movsd xmm2, QWORD PTR [rsp+8] ; restore
jmp .L15
2 memory ops. It kept rbp/r13/rbx/r12 as callee-saved registers across
the loop, so the loop-carried values survive the call for free.
defaulted (time 37.9) — spills five values:
asm
.L17: mov QWORD PTR [rsp+24], rcx ; save 'this'
mov DWORD PTR [rsp+20], r8d ; save n
mov DWORD PTR [rsp+16], esi ; save loop counter
mov QWORD PTR [rsp], rdi ; save &calc[k]
movsd QWORD PTR [rsp+8], xmm2 ; save accumulator
call "Calc::update(double, double)"
mov rcx, QWORD PTR [rsp+24] ; restore x5
mov r8d, DWORD PTR [rsp+20]
mov esi, DWORD PTR [rsp+16]
movsd xmm2, QWORD PTR [rsp+8]
mov rdi, QWORD PTR [rsp]
jmp .L15
10 memory ops instead of 2 — every iteration that takes the Calc::update
path pays 8 extra loads/stores.
Because the defaulted variant's loop is estimated as cold (time 37.9),
the register allocator doesn't bother promoting the loop-carried values
to callee-saved registers; it uses caller-saved ones
(rcx/r8/rsi/rdi/r9/r10/r11) and spills them around every call.
The explicit variant, seen as hot, sets up callee-saved registers
in the prologue and the call costs almost nothing.
----
Not a duplicate of PR 108953. That bug is about the defaulted operator's own
body being suboptimal -- multiple member comparisons not being coalesced into
wider loads, for a 7-member struct, contrasted against memcmp and clang.
Here the type has a single int16_t member, the defaulted operator's codegen
is
byte-identical to the hand-written one, and it is inlined away in the early
inliner in both variants. Nothing about the operator's body differs. The
regression is entirely in downstream analysis of the *caller*.
Fixing the coalescing in PR 108953 would not affect this case.