[gcc r17-3250] [PATCH v3] simplify-rtx: widen the memory attributes when folding "c ? a : a" [PR125683]
Jeff Law via Gcc-cvs <[email protected]>
| Newsgroups | gmane.comp.gcc.cvs |
|---|---|
| Message-ID | <[email protected]> |
https://gcc.gnu.org/g:542d7eaf767057fbd224d44dbc1772dea3e41891 commit r17-3250-g542d7eaf767057fbd224d44dbc1772dea3e41891 Author: Rohith Kapelli <[email protected]> Date: Wed Aug 12 22:05:23 2026 -0600 [PATCH v3] simplify-rtx: widen the memory attributes when folding "c ? a : a" [PR125683] simplify_ternary_operation folds IF_THEN_ELSE (cond, a, a) to a using rtx_equal_p, which ignores the memory attributes. Two loads from the same address into the same register are rtx_equal_p even when they have incompatible alias sets, so the fold returned one arm's MEM and the result inherited just that arm's (too narrow) alias set. noce_try_ifelse_collapse builds exactly such an IF_THEN_ELSE from the two sides of long f (int a, void *cc, long *d) { long long c; *d = 0; if (a) c = *(long *) cc; else c = *(long long *) cc; *d = 1; return c; } so ce1 replaced the two loads with a single long long load; a later pass, seeing that a long long load does not alias the long store to *d, deleted the "dead" *d = 0, and with cc == d the function returned a stale value. Rather than dropping the fold when the attributes differ, keep it and return a reference that only claims what both operands guarantee, in the spirit of merge_memattrs: alias set 0 when the sets differ, MEM_EXPR and offset cleared when they disagree, and the minimum alignment. The operands can be shared, so the attributes are set on a shallow copy rather than in place. MEM_READONLY_P, MEM_NOTRAP_P and MEM_POINTER each assert something about the reference, so the copy keeps them only when both operands do. They are rtx flag bits rather than MEM_ATTRS fields, so shallow_copy_rtx takes them from the first operand and they have to be cleared by hand; the equal-attributes early exit tests them too, so a disagreement in a flag alone still goes through the copy. merge_memattrs already drops the first two this way when it commons two references, so this only follows it; it does not look at MEM_POINTER, which is treated the same way here because it is an assertion about the loaded value in just the same sense. Unlike merge_memattrs, which fixes up two references that both remain in the instruction stream, this returns a single reference standing in for either arm, so the size is kept only when both agree instead of taking the larger one. BLKmode is left alone because there MEM_ATTRS describes the size of the access itself. Volatility is not merged: it constrains when the access happens rather than describing the memory, so it can be neither weakened (that would lose a required access) nor strengthened as merge_memattrs does (that would add a volatile access on the arm which did not have one, there being a single access left for either arm), and the fold is declined instead. Address spaces need no check: rtx_equal_p already fails for MEMs in different address spaces. Several tree passes can factor the two loads with a conservative type before RTL and so hide this: PRE and code hoisting on the release branches, and the phi-opt load factoring (PR125557) on trunk. The test disables them so the if-conversion path is exercised on every affected version. It is a live wrong-code at -O2 on the 13/14/15/16 branches -- gcc-13 miscompiles the reduced case at plain -O2 with no such flags at all. --- Changes since v2: MEM_READONLY_P, MEM_NOTRAP_P and MEM_POINTER are now dropped when the two arms disagree. They are rtx flag bits rather than MEM_ATTRS fields, so shallow_copy_rtx brings them across from the first operand and they have to be cleared by hand; they are cleared on the fresh MEM the fold already builds, not on either operand, which can be shared. The equal-attributes early exit tests them too, so a disagreement in a flag alone still goes through the copy. merge_memattrs already clears the first two this way, so MEM_POINTER is the one flag this adds to that treatment. New test gcc.dg/rtl/aarch64/pr125683-flags.c builds the disagreement directly, because it cannot be produced from C source: MEM_READONLY_P and MEM_NOTRAP_P are derived from the base object, which two loads from the same address share. It is gated to aarch64-*-* because rtl.exp collects tests recursively, so every __RTL test has to gate itself, and __RTL tests hard-code target register names. Testing, on aarch64-unknown-linux-gnu: patched and unpatched bootstrapped from clean trees in the same environment, both stage2 == stage3. Comparing every result line across gcc, g++, libstdc++, libgomp, libitm and libatomic -- normalising the build directory, which g++.dg/modules embeds in test names -- the two runs are identical except for this patch's own tests. One apparent transition, g++.dg/tsan/pthread_cond_clockwait.C at -O0, is a flaky execution test: re-run idle eight times per compiler it passed 2/8 unpatched and 1/8 patched. The runtime test is revert-sensitive: the unpatched compiler aborts on it, the patched one exits 0. The patch applies cleanly to trunk and to the gcc-16, gcc-15, gcc-14 and gcc-13 branches. PR rtl-optimization/125683 gcc/ChangeLog: * simplify-rtx.cc (simplify_context::simplify_ternary_operation): When folding an IF_THEN_ELSE of two equal MEM operands with different memory attributes, return a copy whose attributes are widened to what both operands allow. gcc/testsuite/ChangeLog: * gcc.dg/pr125683.c: New test. * gcc.dg/rtl/aarch64/pr125683-flags.c: New test. Signed-off-by: Rohith Kapelli <[email protected]> Diff: --- gcc/simplify-rtx.cc | 73 ++++++++++++++++- gcc/testsuite/gcc.dg/pr125683.c | 38 +++++++++ gcc/testsuite/gcc.dg/rtl/aarch64/pr125683-flags.c | 96 +++++++++++++++++++++++ 3 files changed, 205 insertions(+), 2 deletions(-) diff --git a/gcc/simplify-rtx.cc b/gcc/simplify-rtx.cc index 6f8ee53f209f..658cac29e6aa 100644 --- a/gcc/simplify-rtx.cc +++ b/gcc/simplify-rtx.cc @@ -7649,9 +7649,78 @@ simplify_context::simplify_ternary_operation (rtx_code code, machine_mode mode, if (CONST_INT_P (op0)) return op0 != const0_rtx ? op1 : op2; - /* Convert c ? a : a into "a". */ + /* Convert c ? a : a into "a". Beware that two rtx_equal_p MEMs can + still carry different memory attributes, in particular incompatible + alias sets; returning one of them would narrow the aliasing of the + result to that operand's, which is unsound (PR125683). When the + attributes differ, fold to a copy that keeps only what both operands + guarantee, like merge_memattrs does when cross-jumping commons two + memory references. */ if (rtx_equal_p (op1, op2) && ! side_effects_p (op0)) - return op1; + { + if (op1 == op2 + || !MEM_P (op1) + || (mem_attrs_eq_p (get_mem_attrs (op1), get_mem_attrs (op2)) + && MEM_READONLY_P (op1) == MEM_READONLY_P (op2) + && MEM_NOTRAP_P (op1) == MEM_NOTRAP_P (op2) + && MEM_POINTER (op1) == MEM_POINTER (op2))) + return op1; + + /* For BLKmode the size in MEM_ATTRS describes the access itself, + so it cannot be dropped. Volatility is not merged either: it + constrains when the access happens rather than describing the + memory, so unlike the flags below it cannot be weakened to what + both operands allow. Dropping it would lose a required access; + merge_memattrs and noce_try_cmove_arith instead set it, which is + sound but claims more than either operand did. Those two have to + put something on a reference they are already committed to, while + this fold is free to do nothing, and if-conversion never reaches + it with a volatile operand in any case: side_effects_p is true + for one, so noce_operand_ok rejects it. Decline the fold. */ + if (GET_MODE (op1) != BLKmode + && MEM_VOLATILE_P (op1) == MEM_VOLATILE_P (op2)) + { + rtx mem = shallow_copy_rtx (op1); + + if (MEM_ALIAS_SET (op1) != MEM_ALIAS_SET (op2)) + set_mem_alias_set (mem, 0); + + if (!mem_expr_equal_p (MEM_EXPR (op1), MEM_EXPR (op2))) + { + set_mem_expr (mem, NULL_TREE); + clear_mem_offset (mem); + } + else if (MEM_OFFSET_KNOWN_P (op1) != MEM_OFFSET_KNOWN_P (op2) + || (MEM_OFFSET_KNOWN_P (op1) + && maybe_ne (MEM_OFFSET (op1), MEM_OFFSET (op2)))) + clear_mem_offset (mem); + + /* Unlike merge_memattrs, which fixes up two references that + both stay in the stream, this returns a single reference + that stands in for either arm, so keep the size only when + both agree rather than taking the larger one. */ + if (!MEM_SIZE_KNOWN_P (op1) || !MEM_SIZE_KNOWN_P (op2) + || maybe_ne (MEM_SIZE (op1), MEM_SIZE (op2))) + clear_mem_size (mem); + + set_mem_align (mem, MIN (MEM_ALIGN (op1), MEM_ALIGN (op2))); + + /* MEM_READONLY_P, MEM_NOTRAP_P and MEM_POINTER are rtx flag + bits rather than MEM_ATTRS fields, so shallow_copy_rtx has + already taken them from OP1 and they need clearing by hand. + Each asserts something about the reference, so the copy may + only keep it when both operands do, as merge_memattrs does + for the first two. */ + if (MEM_READONLY_P (op1) != MEM_READONLY_P (op2)) + MEM_READONLY_P (mem) = 0; + if (MEM_NOTRAP_P (op1) != MEM_NOTRAP_P (op2)) + MEM_NOTRAP_P (mem) = 0; + if (MEM_POINTER (op1) != MEM_POINTER (op2)) + MEM_POINTER (mem) = 0; + + return mem; + } + } /* Convert a != b ? a : b into "a". */ if (GET_CODE (op0) == NE diff --git a/gcc/testsuite/gcc.dg/pr125683.c b/gcc/testsuite/gcc.dg/pr125683.c new file mode 100644 index 000000000000..1f5706261b8f --- /dev/null +++ b/gcc/testsuite/gcc.dg/pr125683.c @@ -0,0 +1,38 @@ +/* PR rtl-optimization/125683 */ +/* { dg-do run } */ +/* { dg-options "-O2 -fno-tree-pre -fno-code-hoisting -fdisable-tree-phiopt1 -fdisable-tree-phiopt2 -fdisable-tree-phiopt3 -fdisable-tree-phiopt4 -fdisable-tree-cselim" } */ + +/* if-conversion (ce1) used to collapse the two conditional loads below + into a single load through simplify_gen_ternary's + (if_then_else c X X) -> X rule, which ignores the memory attributes. + The two loads have incompatible alias sets, so the collapsed load kept + just one of them; a later pass then treated it as not aliasing the + long store to *d and moved it, giving the wrong value when cc == d. + Several tree passes (PRE/code-hoisting on the release branches, phi-opt + load factoring on trunk) can factor the two loads with a conservative + type before RTL and so hide the bug; they are disabled here so the + if-conversion path is exercised on every affected version. */ + +long __attribute__ ((noipa)) +f (int a, void *cc, long *d) +{ + long long c; + *d = 0; + if (a) + c = *(long *) cc; + else + c = *(long long *) cc; + *d = 1; + return c; +} + +int +main (void) +{ + long storage = -1; + /* cc == d, and a != 0 so the taken load reads *(long *)cc, which is a + type-compatible access to the object *d that was just set to 0. */ + if (f (1, &storage, &storage) != 0) + __builtin_abort (); + return 0; +} diff --git a/gcc/testsuite/gcc.dg/rtl/aarch64/pr125683-flags.c b/gcc/testsuite/gcc.dg/rtl/aarch64/pr125683-flags.c new file mode 100644 index 000000000000..11b2b77d6b4d --- /dev/null +++ b/gcc/testsuite/gcc.dg/rtl/aarch64/pr125683-flags.c @@ -0,0 +1,96 @@ +/* { dg-do compile { target aarch64-*-* } } */ +/* { dg-options "-O2 -fdump-rtl-ce1" } */ + +/* PR125683: when ce1 collapses "c ? a : a" the folded MEM must not claim + anything only one arm guaranteed. MEM_READONLY_P and MEM_NOTRAP_P + disagreement cannot be produced from C source -- both are derived from + the base object, which two loads from the same address share -- so the + two MEMs are built here directly. The else arm is the one the fold + copies, so it is the arm that carries the flags. + + Based on the ce1 input for + + long f (int a, void *cc, long *d) + { long c; *d = 0; if (a) c = *(long *) cc; else c = *(long *) cc; + *d = 1; return c; } */ + +long __RTL (startwith ("ce1")) f (int a, void *cc, long *d) +{ +(function "f" + (param "a" + (DECL_RTL (reg/v:SI <3> [ a ])) + (DECL_RTL_INCOMING (reg:SI x0 [ a ]))) + (param "cc" + (DECL_RTL (reg/v/f:DI <4> [ cc ])) + (DECL_RTL_INCOMING (reg:DI x1 [ cc ]))) + (param "d" + (DECL_RTL (reg/v/f:DI <5> [ d ])) + (DECL_RTL_INCOMING (reg:DI x2 [ d ]))) + (insn-chain + (block 2 + (edge-from entry (flags "FALLTHRU")) + (cnote 6 [bb 2] NOTE_INSN_BASIC_BLOCK) + (cinsn 2 (set (reg/v:SI <3> [ a ]) + (reg:SI x0 [ a ]))) + (cinsn 3 (set (reg/v/f:DI <4> [ cc ]) + (reg:DI x1 [ cc ]))) + (cinsn 4 (set (reg/v/f:DI <5> [ d ]) + (reg:DI x2 [ d ]))) + (cnote 5 NOTE_INSN_FUNCTION_BEG) + (cinsn 8 (set (mem:DI (reg/v/f:DI <5> [ d ]) [3 S8 A64]) + (const_int 0))) + (cinsn 9 (set (reg:CC cc) + (compare:CC (reg/v:SI <3> [ a ]) + (const_int 0)))) + (cjump_insn 10 (set (pc) + (if_then_else (eq (reg:CC cc) + (const_int 0)) + (label_ref 15) + (pc)))) + (edge-to 3 (flags "FALLTHRU")) + (edge-to 4) + ) ;; block 2 + (block 3 + (edge-from 2 (flags "FALLTHRU")) + (cnote 11 [bb 3] NOTE_INSN_BASIC_BLOCK) + (cinsn 12 (set (reg/v:DI <2>) + (mem:DI (reg/v/f:DI <4> [ cc ]) [2 S8 A64]))) + (edge-to 5 (flags "FALLTHRU")) + ) ;; block 3 + (block 4 + (edge-from 2) + (clabel 15 2) + (cnote 16 [bb 4] NOTE_INSN_BASIC_BLOCK) + (cinsn 17 (set (reg/v:DI <2>) + (mem/u/c/f:DI (reg/v/f:DI <4> [ cc ]) [1 S8 A64]))) + (edge-to 5 (flags "FALLTHRU")) + ) ;; block 4 + (block 5 + (edge-from 3 (flags "FALLTHRU")) + (edge-from 4 (flags "FALLTHRU")) + (cnote 20 [bb 5] NOTE_INSN_BASIC_BLOCK) + (cinsn 19 (set (reg:DI <6>) + (const_int 1))) + (cinsn 21 (set (mem:DI (reg/v/f:DI <5> [ d ]) [3 S8 A64]) + (reg:DI <6>))) + (cinsn 22 (set (reg/i:DI x0) + (reg/v:DI <2>))) + (cinsn 23 (use (reg/i:DI x0))) + (edge-to exit (flags "FALLTHRU")) + ) ;; block 5 + ) ;; insn-chain +) ;; function +} + +/* The diamond must actually be collapsed, otherwise the rest proves + nothing. */ +/* { dg-final { scan-rtl-dump "noce_try_ifelse_collapse" "ce1" } } */ + +/* The alias sets disagree, so the fold drops to alias set 0. That MEM is + the folded one; it must carry no flag bits, since each was set on only + one arm. Before the flags were handled it came out as + "(mem/u/c:DI (reg...) [0 S8 A64])". The dump also lists the pass input, + where the /u/c arm legitimately appears, so both checks are anchored on + the alias set 0 that only the folded MEM has. */ +/* { dg-final { scan-rtl-dump "mem:DI \\(reg\[^\)\]*\\) \\\[0 " "ce1" } } */ +/* { dg-final { scan-rtl-dump-not "mem/\[a-z/\]*:DI \\(reg\[^\)\]*\\) \\\[0 " "ce1" } } */