https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127938

            Bug ID: 127938
           Summary: single_set_2 counts debug uses with DF_REG_USE_COUNT,
                    so a debug bind of an unused PARALLEL output changes
                    code
           Product: gcc
           Version: 17.0
            Status: UNCONFIRMED
          Keywords: compare-debug-failure
          Severity: normal
          Priority: P3
         Component: rtl-optimization
          Assignee: unassigned at gcc dot gnu.org
          Reporter: yqtian668 at gmail dot com
  Target Milestone: ---
              Host: x86_64-pc-linux-gnu
            Target: x86_64-pc-linux-gnu
             Build: GCC 17.0.0 (experimental),
                    84348add236100e5f69a3e9ec4f7b1038bf2b83d

Created attachment 65870
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65870&action=edit
GCC plugin reproducer (bindquot.cc)

Since r16-6062 (PR 121852, "rtlanal: Use REG_UNUSED notes in single_set
only in passes where df_analyze has computed them"), `single_set_2` decides
whether a second `SET` of a `PARALLEL` is dead, in passes that have `df`
but no note problem, as follows:

```c
/* gcc/rtlanal.cc, single_set_2 */
              auto unused = [] (const rtx_insn *insn, rtx dest) {
                if (!df)
                  return false;
                if (df_note)
                  return !!find_reg_note (insn, REG_UNUSED, dest);
                return (REG_P (dest)
                        && !HARD_REGISTER_P (dest)
                        && REGNO (dest) < df->regs_inited
                        && DF_REG_USE_COUNT (REGNO (dest)) == 0);
              };
```

`DF_REG_USE_COUNT` includes uses in debug insns.
`regstat_init_n_sets_and_refs` subtracts them for `REG_N_REFS` for this
reason. The REG_UNUSED notes used before r16-6062 ignore debug uses as
well. So if a debug insn binds a variable to the otherwise unused output of a
multi-SET PARALLEL, `single_set` returns NULL with `-g` and the live SET
with `-g0`. Every caller of `single_set` in those passes can then decide
differently. Such passes include fwprop, cprop, `loop2_invariant`, ifcvt,
`late_combine`, `ree` and `ext_dce`.

The example below is on x86_64 at `-O2`. `1 % a` in
`gcc.dg/torture/pr54458.c` expands to `*udivmodsi4`, whose quotient pseudo
is unused:

```
(parallel [(set (reg:SI 111) (udiv:SI (reg:SI 109) (reg:SI 100)))
           (set (reg:SI 110) (umod:SI (reg:SI 109) (reg:SI 100)))
           (clobber (reg:CC flags))])
```

With `-g0`, `loop2_invariant` treats the insn as a single set of `reg 110`
and hoists it out of the loop. With one debug bind of `reg 111`,
`single_set` returns NULL, the insn stays in the loop, and the final code
differs.

## Reproducer

I have not found a C or C++ source that makes GCC emit such a bind by
itself. A small plugin adds one before `loop2_invariant`. Under `-g0` it
does nothing, so `-fcompare-debug` compares the compilation with the bind
against the compilation without it.

I am not sure whether it is a bug or not, so any suggestion would be
appreciated. 


`bindquot.cc`:

```c++
/* bindquot.cc: GCC plugin for the single_set_2 report.  Before
   loop2_invariant, after every two-SET PARALLEL whose first SET is a pseudo,
   add one debug bind of an artificial variable to that pseudo.  Under -g0
   (no debug bind insns) it does nothing, so -fcompare-debug compares the
   compilation with the bind against the one without it.  */
#include "gcc-plugin.h"
#include "plugin-version.h"
#include "system.h"
#include "coretypes.h"
#include "backend.h"
#include "tree.h"
#include "rtl.h"
#include "memmodel.h"
#include "emit-rtl.h"
#include "langhooks.h"
#include "tree-pass.h"
#include "context.h"
#include "stringpool.h"

int plugin_is_GPL_compatible;

namespace {
const pass_data data = { RTL_PASS, "bindquot", OPTGROUP_NONE, TV_NONE,
                         PROP_rtl, 0, 0, 0, 0 };

struct bindquot_pass : rtl_opt_pass
{
  bindquot_pass (gcc::context *c) : rtl_opt_pass (data, c) {}
  unsigned int execute (function *fun) final override
  {
    if (!MAY_HAVE_DEBUG_BIND_INSNS)
      return 0;
    basic_block bb;
    rtx_insn *insn;
    FOR_EACH_BB_FN (bb, fun)
      FOR_BB_INSNS (bb, insn)
        {
          if (!NONDEBUG_INSN_P (insn) || GET_CODE (PATTERN (insn)) != PARALLEL)
            continue;
          rtx pat = PATTERN (insn);
          if (XVECLEN (pat, 0) < 2
              || GET_CODE (XVECEXP (pat, 0, 0)) != SET
              || GET_CODE (XVECEXP (pat, 0, 1)) != SET)
            continue;
          rtx dest = SET_DEST (XVECEXP (pat, 0, 0));
          if (!REG_P (dest) || HARD_REGISTER_P (dest))
            continue;
          tree var = build_decl (UNKNOWN_LOCATION, VAR_DECL,
                                 get_identifier ("quot"),
                                 lang_hooks.types.type_for_mode (GET_MODE
(dest),
                                                                 1));
          DECL_CONTEXT (var) = current_function_decl;
          DECL_ARTIFICIAL (var) = 1;
          emit_debug_insn_after (gen_rtx_VAR_LOCATION (GET_MODE (dest), var,
                                                       dest,
                                                      
VAR_INIT_STATUS_INITIALIZED),
                                 insn);
        }
    return 0;
  }
};
}

int
plugin_init (struct plugin_name_args *info, struct plugin_gcc_version *)
{
  struct register_pass_info p;
  p.pass = new bindquot_pass (g);
  p.reference_pass_name = "loop2_invariant";
  p.ref_pass_instance_number = 1;
  p.pos_op = PASS_POS_INSERT_BEFORE;
  register_callback (info->base_name, PLUGIN_PASS_MANAGER_SETUP, NULL, &p);
  return 0;
}
```

`pr54458.c` (`gcc.dg/torture/pr54458.c`):

```c
/* { dg-do compile } */

unsigned int a, b, c;

void
foo (unsigned int x)
{
  do
    {
      if (a == 0 ? 1 : 1 % a)
        for (; b; b--)
          lab:;
      else
        while (x)
          ;
      if (c)
        goto lab;
    }
  while (1);
}
```

Commands (x86_64-pc-linux-gnu, master
84348add236100e5f69a3e9ec4f7b1038bf2b83d):

```sh
g++ -std=c++17 -shared -fPIC -fno-rtti \
  -I"$(gcc -print-file-name=plugin)/include" bindquot.cc -o bindquot.so
gcc -O2 -fcompare-debug -fplugin=./bindquot.so -S pr54458.c
```

Actual result:

```text
gcc: error: pr54458.c: '-fcompare-debug' failure (length)
```

Without the plugin, or with the change below, the compilation succeeds.
The `loop2_invariant` dump shows the difference:

```text
-g0:  Set in insn 23 is invariant (0), cost 4, depends on
      Set in insn 24 is invariant (1), cost 6, depends on 0
-g:   Set in insn 25 is invariant (0), cost 4, depends on
```

Insn 24 (`-g0`) is the divmod. With `-g` it is no longer an invariant
candidate. The register allocation, block layout and position of the `divl`
in the final assembly change too.

## Possible fix

Count only uses outside debug insns, as `regstat_init_n_sets_and_refs`
does:

```diff
--- a/gcc/rtlanal.cc
+++ b/gcc/rtlanal.cc
@@ -1563,10 +1563,17 @@
                  return false;
                if (df_note)
                  return !!find_reg_note (insn, REG_UNUSED, dest);
-               return (REG_P (dest)
-                       && !HARD_REGISTER_P (dest)
-                       && REGNO (dest) < df->regs_inited
-                       && DF_REG_USE_COUNT (REGNO (dest)) == 0);
+               if (!REG_P (dest)
+                   || HARD_REGISTER_P (dest)
+                   || REGNO (dest) >= df->regs_inited)
+                 return false;
+               /* Uses in debug insns do not make the SET live.  */
+               for (df_ref use = DF_REG_USE_CHAIN (REGNO (dest)); use;
+                    use = DF_REF_NEXT_REG (use))
+                 if (DF_REF_IS_ARTIFICIAL (use)
+                     || !DEBUG_INSN_P (DF_REF_INSN (use)))
+                   return false;
+               return true;
              };
              if (!set_verified)
                {
```

With this change, the reproducer passes `-fcompare-debug`. The `-g0`
assembly is byte-identical to that of the unpatched compiler for the 5,235
files of the C torture tests and `gcc.dg/torture` that compile at `-O2`
without their dg-options. I have not bootstrapped or regression-tested it.

Walking the use chain makes `single_set` more expensive in these passes.
A cheaper alternative may be preferable, for example caching the count of
non-debug uses.

Reply via email to