On Sun, 6 Sep 2026, Rohith Kapelli wrote:
> func_checker::compare_operand compares non-memory operands with a bare
> operand_equal_p and OEP_MATCH_SIDE_EFFECTS. When the operand is an
> ADDR_EXPR, operand_equal_p recurses with OEP_ADDRESS_OF set, and for a
> COMPONENT_REF under OEP_ADDRESS_OF it accepts two different FIELD_DECLs
> whose DECL_FIELD_OFFSET and DECL_FIELD_BIT_OFFSET agree. That is correct
> for the value of the address, but ICF is not only comparing an address.
> The surviving body carries one of the two field types into every caller,
> and get_range_strlen_tree reads TYPE_DOMAIN and TYPE_MAX_VALUE off it.
>
> In the PR two structs hold a char array as their first member, one of 11
> and one of 128 elements, with identical operator== bodies. ICF unifies
> them, the surviving body carries char[11], and the strlen machinery then
> concludes that a 15 character string cannot be there.
>
> Pass OEP_ADDRESS_OF_SAME_FIELD, which tree-core.h already documents for
> this case: "For OEP_ADDRESS_OF of COMPONENT_REFs, only consider same
> fields as equivalent rather than also different fields with the same
> offset." fold-const.cc honours it at the point where the two offsets
> would otherwise be accepted.
>
> PR ipa/119006
>
> gcc/ChangeLog:
>
> * ipa-icf-gimple.cc (func_checker::compare_operand): Pass
> OEP_ADDRESS_OF_SAME_FIELD to operand_equal_p.
>
> gcc/testsuite/ChangeLog:
>
> * g++.dg/ipa/pr119006.C: New test.
>
> Signed-off-by: Rohith Kapelli <[email protected]>
> ---
> PR119006 is wrong code, filed 2025-02-25 and live on gcc-13, gcc-14,
> gcc-15, gcc-16 and trunk. Two structs hold a char array as their first
> member, one char[11] and one char[128], with identical operator== bodies.
> ICF unifies the two operator==, the surviving body carries char[11], and
> get_range_strlen_tree reads TYPE_DOMAIN off that type and concludes that a
> 15 character string cannot be there. The comparison folds to a constant and
> the program takes the wrong branch.
>
> The mechanism is in func_checker::compare_operand. Non-memory operands go
> through a bare operand_equal_p with OEP_MATCH_SIDE_EFFECTS. For an
> ADDR_EXPR operand_equal_p recurses with OEP_ADDRESS_OF set, and for a
> COMPONENT_REF under OEP_ADDRESS_OF fold-const.cc accepts two different
> FIELD_DECLs when DECL_FIELD_OFFSET and DECL_FIELD_BIT_OFFSET agree. Both
> _str members sit at offset 0, so the fields compare equal and the functions
> are unified.
>
> Comment 10 asks for a flag that would check the MEM_REF details even under
> OEP_ADDRESS_OF. The divergence here is one step before that: field identity
> on the COMPONENT_REF at fold-const.cc:3599, not a MEM_REF detail.
>
> GCC already documents a flag for that fallback, on every branch this patch
> targets and gcc-13 included. tree-core.h:
>
> /* For OEP_ADDRESS_OF of COMPONENT_REFs, only consider same fields as
> equivalent rather than also different fields with the same offset. */
> OEP_ADDRESS_OF_SAME_FIELD = 256,
>
> It has one consumer, fold-const.cc:3599, and three callers, all of them in
> front ends: cp/call.cc, c-family/c-warn.cc and c/c-typeck.cc, none in the
> middle end. The patch passes it from compare_operand.
>
> Comment 12 asks how often this fires, and notes that merging semantically
> equivalent functions from different template instantiations is an important
> case of ICF, over 20 percent of LibreOffice code size.
>
> Here is that measurement, and the patch is not free. On
> libstdc++-v3/src/c++{98,11,17,20}/*.cc compiled to objects and linked as
> one shared library with LTO, 105 objects, it declines 25 merges at -O2 and
> 29 at -Os while gaining 3 at each level, so 25 of 255 and 29 of 270. Read
> off the assembler names in the WPA dump they are almost entirely char
> against wchar_t instantiations of stream and locale accessors, rdbuf 9,
> is_open 7, do_is_equal 4, operator= 2, fd and file 2, one transactional
> error message getter, and one pair from two different class templates
> sharing an accessor body. That is the cross-instantiation class comment 12
> calls important, and what it costs is 264 bytes of .text at -O2 and 2188 at
> -Os, against 1710731 and 1365540 bytes unpatched, so 0.015 and 0.16
> percent.
>
> An instrumented compiler that logs this divergence emitted 0 lines over the
> same workload at both levels, 525 merges. Its sensitivity was established
> first on four constructed divergence shapes and it fires on all four. That
> zero is the rate at which this bug fires on this workload, not a claim that
> ICF never merges across divergent metadata.
>
> The workload is one library, and libstdc++'s own sources are library
> implementation rather than application-scale template instantiation, so
> this speaks to comment 12's reference point without reaching it. No
> LibreOffice-scale C++ body was available here to measure.
>
> Comment 12 offers two routes, giving up the merge or modifying the merged
> body into a form safe for both. This patch takes the first. I prototyped
> the second, rewriting the diverging ADDR_EXPR to a MEM_REF and retyping the
> SSA name: it fixes all 40 configurations while keeping the merge, recovers
> trunk's 255 and 270 merges exactly, and bootstraps, but it fires zero times
> across those 525 merges, so every aggregate result validated it dormant and
> the finding rests on one instance. Feasibility, not a proposal.
>
> Testing, on aarch64-unknown-linux-gnu at trunk 95c85b404da, patched and
> unpatched from the same tree in the same environment with only
> ipa-icf-gimple.cc differing. Three stage bootstrap configured
> --enable-languages=c,c++,lto --enable-checking=yes,rtl,extra
> --disable-multilib --disable-nls --disable-libsanitizer, make compare
> reported comparison successful. Shard sums merged with
> contrib/dg-extract-results.sh:
>
> tool variant total PASS FAIL UNRESOLVED
> gcc trunk 408405 401230 71 0
> gcc patch 408405 401230 71 0
> g++ trunk 474714 468765 52 0
> g++ patch 474777 468828 52 0
>
> The gcc suite is identical line for line. The g++ totals differ by 63 test
> names, which is g++.dg/modules churn that appears between unpatched runs as
> well; no FAIL and no UNRESOLVED is added or removed.
>
> Runtime libraries, unpatched against patched, comparing sorted PASS, FAIL,
> XPASS and UNRESOLVED lines: libstdc++ 18914 PASS and 1 FAIL, libgomp 6273,
> libitm 44, libatomic 54, and 0 differing lines in all four. The libstdc++
> FAIL is experimental/net/internet/resolver/ops/lookup.cc, which needs DNS
> the test machine does not have, and is present unpatched.
>
> A 40 configuration reproducer matrix, 2 testcases by 5 optimisation levels
> by 4 flag sets, goes from 6 failing to 0. The same matrix on the release
> branches with the patch applied to each:
>
> gcc-16 bf726b75da7 6 failing before, 0 after
> gcc-15 ae79106ea90 6 failing before, 0 after
> gcc-14 8c5c1a3eca2 6 failing before, 0 after
> gcc-13 e1926c31dcb 6 failing before, 0 after
>
> On gcc-13 the hunk applies at offset -1 lines.
>
> The testcase is revert sensitive. One build tree, patch applied, then
> reverted in place with only the compiler rebuilt, then reapplied:
>
> applied 12 PASS, 0 FAIL
> reverted 3 PASS, 9 FAIL
> reapplied 12 PASS, 0 FAIL
>
> The 3 that pass unpatched are the excess errors lines, one per variant.
> Variants exercised are gnu++11, gnu++20 and gnu++29.
>
> On the choice: Jakub's comment 11 patch 1 also fixes this, by intercepting
> ADDR_EXPR and comparing the operands without OEP_ADDRESS_OF, which restores
> the same field rule and the MEM_REF checks too. On libstdc++ it declines
> the identical set and produces the same .text, and I could not separate the
> two on evidence: across three constructed shapes at four option
> combinations every alignment sensitive pair was rejected by compare_ao_refs
> before the blind operand_equal_p was reached, and three shapes is not a
> proof. I bootstrapped and regression tested his patch here too, with the
> same results, and the testcase is revert sensitive against it in the same
> way. If it is preferred, it is his and it is ready, the testing above
> applies to it unchanged, and I have no argument against it.
>
> Where would this fire at scale, since libstdc++ gives zero?
> Must such a rewrite work at WPA, given 8a917760aa4 takes a !flag_wpa exit?
> Is retyping an SSA name in place acceptable, or must consumers be checked?
>
> OK for trunk?
OK.
Thanks,
Richard.
> gcc/ipa-icf-gimple.cc | 3 ++-
> gcc/testsuite/g++.dg/ipa/pr119006.C | 33 +++++++++++++++++++++++++++++
> 2 files changed, 35 insertions(+), 1 deletion(-)
> create mode 100644 gcc/testsuite/g++.dg/ipa/pr119006.C
>
> diff --git a/gcc/ipa-icf-gimple.cc b/gcc/ipa-icf-gimple.cc
> index a893a3faaa4..720a3a0ce1a 100644
> --- a/gcc/ipa-icf-gimple.cc
> +++ b/gcc/ipa-icf-gimple.cc
> @@ -439,7 +439,8 @@ func_checker::compare_operand (tree t1, tree t2,
> operand_access_type access)
> }
> else
> {
> - if (operand_equal_p (t1, t2, OEP_MATCH_SIDE_EFFECTS))
> + if (operand_equal_p (t1, t2, OEP_MATCH_SIDE_EFFECTS
> + | OEP_ADDRESS_OF_SAME_FIELD))
> return true;
> return return_false_with_msg
> ("operand_equal_p failed");
> diff --git a/gcc/testsuite/g++.dg/ipa/pr119006.C
> b/gcc/testsuite/g++.dg/ipa/pr119006.C
> new file mode 100644
> index 00000000000..aa975eb3004
> --- /dev/null
> +++ b/gcc/testsuite/g++.dg/ipa/pr119006.C
> @@ -0,0 +1,33 @@
> +// PR ipa/119006
> +// { dg-do run { target c++11 } }
> +// { dg-options "-O2 -fwhole-program -fdump-ipa-icf-details" }
> +
> +struct FixedString10 {
> + bool operator==(const char* rhs_) const { return rhs_ and not
> __builtin_strcmp(_str, rhs_); }
> + char _str[11];
> +};
> +struct FixedString127 {
> + bool operator==(const char* rhs_) const { return rhs_ and not
> __builtin_strcmp(_str, rhs_); }
> + bool operator!=(const char* rhs_) const { return !(*this == rhs_); }
> + char _str[128];
> +};
> +[[gnu::noinline,gnu::used]]
> +int fixedStringUser(const FixedString10& lhs, const char* rhs) {
> + return lhs == rhs;
> +}
> +constexpr const char *t = "StrOverTenChars";
> +[[gnu::noinline,gnu::used]]
> +void checkString(FixedString127 reason_) {
> + if (reason_ != t) __builtin_trap();
> +}
> +
> +int main(int argc, char** argv) {
> + FixedString127 fs;
> + __builtin_strcpy(fs._str, t);
> + checkString(fs);
> +}
> +
> +// The two operator== bodies are identical but their _str members have
> +// different bounds, so ICF must not treat them as equal.
> +// { dg-final { scan-ipa-dump-not "Unified" "icf" } }
> +// { dg-final { scan-ipa-dump "Equal symbols: 0" "icf" } }
>
--
Richard Biener <[email protected]>
SUSE Software Solutions Germany GmbH,
Frankenstrasse 146, 90461 Nuernberg, Germany;
GF: Jochen Jaser, Andrew McDonald, Abhinav Puri; (HRB 36809, AG Nuernberg)