From: Kyrylo Tkachov <[email protected]>
If-conversion emits its replacement sequence at the end of the test block.
Expanding a conditional move there can require a fresh comparison, which
writes a condition-code register. end_ifcvt_sequence already rejects a
sequence that would destroy the condition code tested by the branch, but
cc_in_cond only reports that register when the branch reads it directly.
A branch such as AArch64 CBZ tests a general register, so the guard was
inert and an unrelated live condition code could be destroyed.
For the testcase at -O2 on aarch64, late-combine sinks a cset into the
join block, which leaves the flags live across the branch:
bb2: cmp w3, 1 // sets cc
cset w4, ls
cbz w1, .L2 // does not touch cc
bb5: cinc w2, w2, ls // reads cc
ce2 then if-converted bb3 and bb4 and inserted "cmp w1, 0" ahead of the
branch, so cinc read the wrong flags:
cmp w3, 1
cset w4, ls
cmp w1, 0 // clobbers the live flags
csel w2, w2, w3, eq
cinc w2, w2, ls // reads cmp w1, 0
rtl.h documents that ports in the "lowered" form, which includes aarch64
before register allocation, may keep the flags live between instructions,
so if-conversion has to respect that. Reject a generated sequence that
writes a condition-code register while it is live on exit from the test
block. Condition codes are recognised by mode class, as in cc_in_cond,
which covers ports that do not define TARGET_FLAGS_REGNUM. DF liveness is
already up to date here and the same paths query it for pseudos.
noce_convert_multiple_sets validates its sequence itself rather than
through end_ifcvt_sequence, and reaches noce_emit_cmove in the same way,
so it gets the same check.
PR126747 is the same defect reached from a different direction. At -Os the
multiplication overflow idiom becomes one .MUL_OVERFLOW, so both arms read a
single cset, and late-combine folds it into the second one:
bb2: cmp xzr, x0, lsr 32 // sets cc
cset w3, ne
cbz w0, .L4 // does not touch cc
bb5: cinc w0, w0, ne // reads cc
ce2 if-converted bb3 the same way and "cmp w0, 0" landed ahead of the cinc, so
foo (1, 1) returned 1 instead of 0.
Bootstrapped and tested on aarch64-none-linux.
Ok for trunk?
Thanks,
Kyrill
gcc/ChangeLog:
PR rtl-optimization/126501
PR rtl-optimization/126747
* ifcvt.cc (noce_clobbers_live_cc_p): New function.
(end_ifcvt_sequence): Use it to reject sequences that clobber a
condition-code register that is live out of the test block.
(noce_convert_multiple_sets): Likewise.
gcc/testsuite/ChangeLog:
PR rtl-optimization/126501
PR rtl-optimization/126747
* gcc.c-torture/execute/pr126501.c: New test.
* gcc.c-torture/execute/pr126747.c: New test.
Signed-off-by: Kyrylo Tkachov <[email protected]>
---
gcc/ifcvt.cc | 37 +++++++++++
.../gcc.c-torture/execute/pr126501.c | 64 +++++++++++++++++++
.../gcc.c-torture/execute/pr126747.c | 26 ++++++++
3 files changed, 127 insertions(+)
create mode 100644 gcc/testsuite/gcc.c-torture/execute/pr126501.c
create mode 100644 gcc/testsuite/gcc.c-torture/execute/pr126747.c
diff --git a/gcc/ifcvt.cc b/gcc/ifcvt.cc
index 5ea25f8fbe7..dbff3727706 100644
--- a/gcc/ifcvt.cc
+++ b/gcc/ifcvt.cc
@@ -1094,6 +1094,34 @@ cc_in_cond (rtx cond)
return NULL_RTX;
}
+/* Return true if SEQ writes a condition-code register that still holds a
+ value which is live on exit from TEST_BB.
+
+ An if-converted sequence is emitted immediately before the jump that ends
+ TEST_BB, and the conversion then removes that jump, so a condition code
+ that is live out of TEST_BB is one which a later block still reads.
+ Expanding a conditional move can emit a fresh comparison, and that would
+ destroy it. */
+
+static bool
+noce_clobbers_live_cc_p (basic_block test_bb, rtx_insn *seq)
+{
+ HARD_REG_SET written;
+ CLEAR_HARD_REG_SET (written);
+ for (rtx_insn *insn = seq; insn; insn = NEXT_INSN (insn))
+ note_stores (insn, record_hard_reg_sets, &written);
+
+ bitmap live_out = df_get_live_out (test_bb);
+ hard_reg_set_iterator hrsi;
+ unsigned int regno;
+ EXECUTE_IF_SET_IN_HARD_REG_SET (written, 0, regno, hrsi)
+ if (GET_MODE_CLASS (reg_raw_mode[regno]) == MODE_CC
+ && bitmap_bit_p (live_out, regno))
+ return true;
+
+ return false;
+}
+
/* Return sequence of instructions generated by if conversion. This
function calls end_sequence() to end the current stream, ensures
that the instructions are unshared, recognizable non-jump insns.
@@ -1128,6 +1156,10 @@ end_ifcvt_sequence (struct noce_if_info *if_info)
|| (cc && set_of (cc, insn)))
return NULL;
+ /* CC above only covers the condition code read by the converted branch. */
+ if (noce_clobbers_live_cc_p (if_info->test_bb, seq))
+ return NULL;
+
return seq;
}
@@ -4011,6 +4043,11 @@ noce_convert_multiple_sets (struct noce_if_info *if_info)
|| recog_memoized (insn) == -1)
return false;
+ /* This path does not go through end_ifcvt_sequence, so apply the same rule
+ about condition codes that outlive the if-region. */
+ if (noce_clobbers_live_cc_p (test_bb, seq))
+ return false;
+
emit_insn_before_setloc (seq, if_info->jump,
INSN_LOCATION (insn_info.last ()->unmodified_insn));
diff --git a/gcc/testsuite/gcc.c-torture/execute/pr126501.c
b/gcc/testsuite/gcc.c-torture/execute/pr126501.c
new file mode 100644
index 00000000000..c86f195ba2b
--- /dev/null
+++ b/gcc/testsuite/gcc.c-torture/execute/pr126501.c
@@ -0,0 +1,64 @@
+/* PR rtl-optimization/126501 */
+/* If-conversion emitted a fresh comparison at the end of the test block
+ while a condition-code value set earlier in that block was still live
+ in the join block. */
+
+struct F { unsigned int f0 : 2, f1 : 2, f2 : 2; };
+struct G { unsigned int g : 2; };
+struct S { struct F f; struct G g[6]; };
+
+__attribute__((noipa)) int
+f (struct S *p, int x)
+{
+ p->g[1].g = (1 >= p->f.f1);
+ p->g[2].g = x ? p->f.f0 : p->f.f2;
+ p->g[0].g = p->g[1].g + p->g[2].g;
+ return p->g[0].g;
+}
+
+__attribute__((noipa)) int
+ref (struct S *p, int x)
+{
+ volatile int f0 = p->f.f0, f1 = p->f.f1, f2 = p->f.f2;
+ volatile int a = (1 >= f1);
+ volatile int b = (x ? f0 : f2);
+
+ p->g[1].g = a;
+ p->g[2].g = b;
+ p->g[0].g = (int) p->g[1].g + (int) p->g[2].g;
+ return p->g[0].g;
+}
+
+__attribute__((noipa)) int
+opaque (int v)
+{
+ return v;
+}
+
+int
+main (void)
+{
+ struct S s;
+ int i, j, k, xi;
+
+ for (i = 0; i < 4; i++)
+ for (j = 0; j < 4; j++)
+ for (k = 0; k < 4; k++)
+ for (xi = 0; xi < 2; xi++)
+ {
+ int x = opaque (xi);
+ int got, want;
+
+ __builtin_memset (&s, 0, sizeof s);
+ s.f.f0 = i; s.f.f1 = j; s.f.f2 = k;
+ got = f (&s, x);
+
+ __builtin_memset (&s, 0, sizeof s);
+ s.f.f0 = i; s.f.f1 = j; s.f.f2 = k;
+ want = ref (&s, x);
+
+ if (got != want)
+ __builtin_abort ();
+ }
+ return 0;
+}
diff --git a/gcc/testsuite/gcc.c-torture/execute/pr126747.c
b/gcc/testsuite/gcc.c-torture/execute/pr126747.c
new file mode 100644
index 00000000000..f9e59701915
--- /dev/null
+++ b/gcc/testsuite/gcc.c-torture/execute/pr126747.c
@@ -0,0 +1,26 @@
+/* PR rtl-optimization/126747 */
+/* Both overflow tests read one condition-code value that late-combine left
+ live across the branches. If-conversion emitted a fresh comparison at the
+ end of the test block and destroyed it. */
+
+volatile int c[2];
+
+__attribute__((noipa)) int
+foo (unsigned x, unsigned y)
+{
+ unsigned r = x * y;
+ int t = 0;
+ if (c[0]) { int u = 0; if (x != 0) u = (r / x != y); t += u; }
+ if (c[1]) { int u = 0; if (x != 0) u = (r / x != y); t += u; }
+ return t;
+}
+
+int
+main (void)
+{
+ c[0] = 1;
+ c[1] = 1;
+ if (foo (1, 1) != 0)
+ __builtin_abort ();
+ return 0;
+}
--
2.50.1 (Apple Git-155)