]> git.ipfire.org Git - thirdparty/gcc.git/commitdiff
tailc: Deal with trivially useless EH cleanups [PR119491]
authorJakub Jelinek <jakub@redhat.com>
Wed, 2 Apr 2025 18:02:34 +0000 (20:02 +0200)
committerJakub Jelinek <jakub@gcc.gnu.org>
Wed, 2 Apr 2025 18:02:34 +0000 (20:02 +0200)
The following testcases FAIL, because EH cleanup is performed only before
IPA and then right before musttail pass.
At -O2 etc. (except for -O0/-Og) we handle musttail calls in the tailc
pass though, and we can fail at that point because the calls might appear
to throw internal exceptions which just don't do anything interesting
(perhaps have debug statements or clobber statements in them) before they
continue with resume of the exception (i.e. throw it externally).

As Richi said in the PR (and I agree) that moving passes is risky at this
point, the following patch instead teaches the tail{r,c} and musttail
passes to deal with such extra EDGE_EH edges.

It is fairly simple thing, if we see an EDGE_EH edge from the call we
just look up where it lands and if there are no
non-debug/non-clobber/non-label statements before resx which throws
externally, such edge can be ignored for tail call optimization or
tail recursion.  At other spots I just need to avoid using
single_succ/single_succ_edge because the bb might have another edge -
EDGE_EH.

To make this less risky, this is done solely for the musttail calls for now.

2025-04-02  Jakub Jelinek  <jakub@redhat.com>

PR tree-optimization/119491
* tree-tailcall.cc (single_non_eh_succ_edge): New function.
(independent_of_stmt_p): Use single_non_eh_succ_edge (bb)->dest
instead of single_succ (bb).
(empty_eh_cleanup): New function.
(find_tail_calls): Diagnose throwing of exceptions which do not
propagate only if there are no EDGE_EH successor edges.  If there are
and the call is musttail, use empty_eh_cleanup to find if the cleanup
is not empty.  If not or the call is not musttail, use different
diagnostics.  Set is_noreturn even if there are successor edges.  Use
single_non_eh_succ_edge (abb) instead of single_succ_edge (abb).  Punt
on internal noreturn calls.
(decrease_profile): Don't assert 0 or 1 successor edges.
(eliminate_tail_call): Use
single_non_eh_succ_edge (gsi_bb (t->call_gsi)) instead of
single_succ_edge (gsi_bb (t->call_gsi)).
(tree_optimize_tail_calls_1): Also look into basic blocks with
single succ edge which is EDGE_EH for noreturn musttail calls.

* g++.dg/opt/musttail3.C: New test.
* g++.dg/opt/musttail4.C: New test.
* g++.dg/opt/musttail5.C: New test.

gcc/testsuite/g++.dg/opt/musttail3.C [new file with mode: 0644]
gcc/testsuite/g++.dg/opt/musttail4.C [new file with mode: 0644]
gcc/testsuite/g++.dg/opt/musttail5.C [new file with mode: 0644]
gcc/tree-tailcall.cc

diff --git a/gcc/testsuite/g++.dg/opt/musttail3.C b/gcc/testsuite/g++.dg/opt/musttail3.C
new file mode 100644 (file)
index 0000000..1c4e549
--- /dev/null
@@ -0,0 +1,41 @@
+// PR tree-optimization/119491
+// { dg-do compile { target { external_musttail && c++11 } } }
+// { dg-options "-O2" }
+
+struct A {
+  struct B {};
+  A () {}
+};
+void qux ();
+unsigned char v;
+A w;
+void foo (A);
+
+template <typename T>
+[[gnu::always_inline]] static inline void
+bar (int &)
+{
+}
+
+[[gnu::always_inline]] static inline void
+baz (int *)
+{
+  int r = 0;
+  bar<int> (r);
+}
+
+[[gnu::always_inline]] inline void
+corge (A)
+{
+  if (v)
+    qux ();
+  [[gnu::musttail]] return foo (w);
+}
+
+void
+freddy (A)
+{
+  int t;
+  baz (&t);
+  [[gnu::musttail]] return corge (A{});
+}
diff --git a/gcc/testsuite/g++.dg/opt/musttail4.C b/gcc/testsuite/g++.dg/opt/musttail4.C
new file mode 100644 (file)
index 0000000..ede2959
--- /dev/null
@@ -0,0 +1,35 @@
+// { dg-do compile { target { external_musttail && c++11 } } }
+// { dg-options "-O2 -fexceptions" }
+
+struct S { ~S (); };
+volatile int v;
+struct T { ~T () { v = v + 1; } };
+struct U { ~U () {} };
+int foo ();
+
+int
+bar () noexcept
+{
+  [[gnu::musttail]] return foo ();     // { dg-error "cannot tail-call: call may throw exception that does not propagate" }
+}
+
+int
+baz ()
+{
+  S s;
+  [[gnu::musttail]] return foo ();     // { dg-error "cannot tail-call: other reasons" }
+}
+
+int
+qux ()
+{
+  T t;
+  [[gnu::musttail]] return foo ();     // { dg-error "cannot tail-call: other reasons" }
+}
+
+int
+corge ()
+{
+  U u;
+  [[gnu::musttail]] return foo ();
+}
diff --git a/gcc/testsuite/g++.dg/opt/musttail5.C b/gcc/testsuite/g++.dg/opt/musttail5.C
new file mode 100644 (file)
index 0000000..604dd69
--- /dev/null
@@ -0,0 +1,41 @@
+// PR tree-optimization/119491
+// { dg-do compile { target { external_musttail && c++11 } } }
+// { dg-options "-O2" }
+
+struct A {
+  struct B {};
+  A () {}
+};
+void qux ();
+unsigned char v;
+A w;
+[[noreturn]] void foo (A);
+
+template <typename T>
+[[gnu::always_inline]] static inline void
+bar (int &)
+{
+}
+
+[[gnu::always_inline]] static inline void
+baz (int *)
+{
+  int r = 0;
+  bar<int> (r);
+}
+
+[[gnu::always_inline]] inline void
+corge (A)
+{
+  if (v)
+    qux ();
+  [[gnu::musttail]] return foo (w);
+}
+
+void
+freddy (A)
+{
+  int t;
+  baz (&t);
+  [[gnu::musttail]] return corge (A{});
+}
index 374be2ae6a75997b1eed86fde26fae68ae086ef9..477729ca2137aeed42ee36c3a103ee941aeb3795 100644 (file)
@@ -253,6 +253,23 @@ suitable_for_tail_call_opt_p (gcall *call, bool diag_musttail)
   return true;
 }
 
+/* Return single successor edge ignoring EDGE_EH edges.  */
+
+static edge
+single_non_eh_succ_edge (basic_block bb)
+{
+   edge e, ret = NULL;
+   edge_iterator ei;
+   FOR_EACH_EDGE (e, ei, bb->succs)
+    if ((e->flags & EDGE_EH) == 0)
+      {
+       gcc_assert (ret == NULL);
+       ret = e;
+      }
+  gcc_assert (ret);
+  return ret;
+}
+
 /* Checks whether the expression EXPR in stmt AT is independent of the
    statement pointed to by GSI (in a sense that we already know EXPR's value
    at GSI).  We use the fact that we are only called from the chain of
@@ -279,7 +296,7 @@ independent_of_stmt_p (tree expr, gimple *at, gimple_stmt_iterator gsi,
   /* Mark the blocks in the chain leading to the end.  */
   at_bb = gimple_bb (at);
   call_bb = gimple_bb (gsi_stmt (gsi));
-  for (bb = call_bb; bb != at_bb; bb = single_succ (bb))
+  for (bb = call_bb; bb != at_bb; bb = single_non_eh_succ_edge (bb)->dest)
     bb->aux = &bb->aux;
   bb->aux = &bb->aux;
 
@@ -323,7 +340,7 @@ independent_of_stmt_p (tree expr, gimple *at, gimple_stmt_iterator gsi,
     }
 
   /* Unmark the blocks.  */
-  for (bb = call_bb; bb != at_bb; bb = single_succ (bb))
+  for (bb = call_bb; bb != at_bb; bb = single_non_eh_succ_edge (bb)->dest)
     bb->aux = NULL;
   bb->aux = NULL;
 
@@ -496,6 +513,33 @@ maybe_error_musttail (gcall *call, const char *err, bool diag_musttail)
     }
 }
 
+/* Return true if there is no real work performed in the exception
+   path starting at BB and it will in the end result in external exception.
+   Search at most CNT basic blocks (so that we don't need to do trivial
+   loop discovery).  */
+static bool
+empty_eh_cleanup (basic_block bb, int cnt)
+{
+  if (EDGE_COUNT (bb->succs) > 1)
+    return false;
+
+  for (gimple_stmt_iterator gsi = gsi_after_labels (bb); !gsi_end_p (gsi);
+       gsi_next (&gsi))
+    {
+      gimple *g = gsi_stmt (gsi);
+      if (is_gimple_debug (g) || gimple_clobber_p (g))
+       continue;
+      if (is_gimple_resx (g) && stmt_can_throw_external (cfun, g))
+       return true;
+      return false;
+    }
+  if (!single_succ_p (bb))
+    return false;
+  if (cnt == 1)
+    return false;
+  return empty_eh_cleanup (single_succ (bb), cnt - 1);
+}
+
 /* Argument for compute_live_vars/live_vars_at_stmt and what compute_live_vars
    returns.  Computed lazily, but just once for the function.  */
 static live_vars_map *live_vars;
@@ -646,14 +690,36 @@ find_tail_calls (basic_block bb, struct tailcall **ret, bool only_musttail,
   if ((stmt_could_throw_p (cfun, stmt)
        && !stmt_can_throw_external (cfun, stmt)) || EDGE_COUNT (bb->succs) > 1)
   {
-    if (stmt == last_stmt)
-      maybe_error_musttail (call,
-                           _("call may throw exception that does not "
-                             "propagate"), diag_musttail);
-    else
-      maybe_error_musttail (call, _("code between call and return"),
-                           diag_musttail);
-    return;
+    if (stmt != last_stmt)
+      {
+       maybe_error_musttail (call, _("code between call and return"),
+                             diag_musttail);
+       return;
+      }
+
+    edge e;
+    edge_iterator ei;
+    FOR_EACH_EDGE (e, ei, bb->succs)
+      if (e->flags & EDGE_EH)
+       break;
+
+    if (!e)
+      {
+       maybe_error_musttail (call,
+                             _("call may throw exception that does not "
+                               "propagate"), diag_musttail);
+       return;
+      }
+
+    if (!gimple_call_must_tail_p (call)
+       || !empty_eh_cleanup (e->dest, 20)
+       || EDGE_COUNT (bb->succs) > 2)
+      {
+       maybe_error_musttail (call,
+                             _("call may throw exception caught locally "
+                               "or perform cleanups"), diag_musttail);
+       return;
+      }
   }
 
   /* If the function returns a value, then at present, the tail call
@@ -844,8 +910,7 @@ find_tail_calls (basic_block bb, struct tailcall **ret, bool only_musttail,
   a = NULL_TREE;
   auto_bitmap to_move_defs;
   auto_vec<gimple *> to_move_stmts;
-  bool is_noreturn
-    = EDGE_COUNT (bb->succs) == 0 && gimple_call_noreturn_p (call);
+  bool is_noreturn = gimple_call_noreturn_p (call);
 
   abb = bb;
   agsi = gsi;
@@ -857,8 +922,9 @@ find_tail_calls (basic_block bb, struct tailcall **ret, bool only_musttail,
 
       while (gsi_end_p (agsi))
        {
-         ass_var = propagate_through_phis (ass_var, single_succ_edge (abb));
-         abb = single_succ (abb);
+         edge e = single_non_eh_succ_edge (abb);
+         ass_var = propagate_through_phis (ass_var, e);
+         abb = e->dest;
          agsi = gsi_start_bb (abb);
        }
 
@@ -932,6 +998,11 @@ find_tail_calls (basic_block bb, struct tailcall **ret, bool only_musttail,
   /* See if this is a tail call we can handle.  */
   if (is_noreturn)
     {
+      if (gimple_call_internal_p (call))
+       {
+         maybe_error_musttail (call, _("internal call"), diag_musttail);
+         return;
+       }
       tree rettype = TREE_TYPE (TREE_TYPE (current_function_decl));
       tree calltype = TREE_TYPE (gimple_call_fntype (call));
       if (!VOID_TYPE_P (rettype)
@@ -1193,11 +1264,6 @@ static void
 decrease_profile (basic_block bb, profile_count count)
 {
   bb->count = bb->count - count;
-  if (!single_succ_p (bb))
-    {
-      gcc_assert (!EDGE_COUNT (bb->succs));
-      return;
-    }
 }
 
 /* Eliminates tail call described by T.  TMP_VARS is a list of
@@ -1262,7 +1328,7 @@ eliminate_tail_call (struct tailcall *t, class loop *&new_loop)
   else
     {
       /* Number of executions of function has reduced by the tailcall.  */
-      e = single_succ_edge (gsi_bb (t->call_gsi));
+      e = single_non_eh_succ_edge (gsi_bb (t->call_gsi));
 
       profile_count count = e->count ();
 
@@ -1277,8 +1343,7 @@ eliminate_tail_call (struct tailcall *t, class loop *&new_loop)
        decrease_profile (e->dest, count);
 
       /* Replace the call by a jump to the start of function.  */
-      e = redirect_edge_and_branch (single_succ_edge (gsi_bb (t->call_gsi)),
-                                   first);
+      e = redirect_edge_and_branch (e, first);
     }
   gcc_assert (e);
   PENDING_STMT (e) = NULL;
@@ -1443,7 +1508,9 @@ tree_optimize_tail_calls_1 (bool opt_tailcalls, bool only_musttail,
     {
       basic_block bb;
       FOR_EACH_BB_FN (bb, cfun)
-       if (EDGE_COUNT (bb->succs) == 0)
+       if (EDGE_COUNT (bb->succs) == 0
+           || (single_succ_p (bb)
+               && (single_succ_edge (bb)->flags & EDGE_EH)))
          if (gimple *c = last_nondebug_stmt (bb))
            if (is_gimple_call (c)
                && gimple_call_must_tail_p (as_a <gcall *> (c))