[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" } } */
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.