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

            Bug ID: 127737
           Summary: IRA regional allocation emits redundant spill stores
                    for loop-invariant values across nested loops
           Product: gcc
           Version: 16.2.1
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: rtl-optimization
          Assignee: unassigned at gcc dot gnu.org
          Reporter: stefan at franke dot ms
  Target Milestone: ---

Created attachment 65753
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65753&action=edit
testcase for arm

With default flags on ARM (-O2) and m68k (-O2 -m68020-40), and
presumably on any target where IRA's regional allocation decides to
spill a loop-invariant value, IRA's regional register
allocation splits at nested loop boundaries and treats loop-invariant
values as if they were redefined inside the inner region.  LRA then
emits a store of the value to its spill slot on every exit from the
region, even though the value was not modified.  The store is redundant
and the value is reloaded unchanged on the next iteration.

The pattern is most visible with a value that is defined once before a
nested loop, read on every iteration of the inner loop, and only
redefined in a rarely-taken branch.  IRA's regional split forces the
value into memory at the region boundary, and LRA's spill code places
the store at the boundary rather than at the actual redefinition point.

-fira-region=one avoids the problem by treating the whole function as a
single region: the value is then live across the entire function with a
single definition and a single update point, and LRA places stores only
at those two points.  The reloads remain, because the register file is
genuinely too small to hold all the values, but the redundant stores
are gone.

Reproduces on ARM (Cortex-A9, -O2) and m68k (-O2, m68020-40).
Also seen in zlib's longest_match on m68k; -fira-region=one
improves the benchmark geomean by 4.5% and dhrystone by 21%.

Testcase for ARM GCC:

/* Testcase: eight loop-invariant byte values read on every iteration
   of a nested loop.  With the default -O2 flags (IRA_REGION_MIXED),
   IRA splits the function into regions at the loop boundaries and
   treats the invariant values as if they were redefined inside the
   inner region.  LRA then emits a store of each value to its spill
   slot on every exit from the region, even though the value was not
   modified.  The store is redundant: the value is reloaded unchanged
   on the next iteration.

   Confirmed reproducers:
     - ARM Cortex-A9, -O2, default flags
     - m68k (68020-40), -O2, default flags (also in zlib longest_match)

   Non-reproducer:
     - x86_64, -O2: 15 GPRs are enough to hold all eight bytes, so
       no spill occurs and the pattern does not appear.

   Expected bug: a store of scan_endN to a stack slot at the loop
   boundary, and a reload of that slot at each use inside the loop,
   even though scan_endN is not modified between uses.

   Workaround: -fira-region=one.  This treats the whole function as a
   single region, so the value has a single definition point and a
   single update point, and LRA places stores only at those points.
   The reloads remain (the register file is genuinely too small), but
   the redundant stores are gone.  -fira-region=mixed reproduces the
   bug identically to the default.  */

typedef unsigned char u8;
typedef unsigned int u32;

struct state {
  u8 *window;
  u32 strstart;
  u32 lookahead;
  u32 w_size;
  u32 w_mask;
  u32 *prev;
  u32 max_chain;
  u32 nice_match;
};

__attribute__((noinline))
u32 longest_match(struct state *s, u32 cur_match)
{
  u32 chain_length = s->max_chain;
  u8 *scan = s->window + s->strstart;
  u8 *match;
  int len;
  int best_len = 0;
  int nice_match = s->nice_match;
  u32 limit = s->strstart > (s->w_size - 262) ? s->strstart - (s->w_size - 262)
: 0;
  u32 *prev = s->prev;
  u32 wmask = s->w_mask;
  u8 *strend = s->window + s->strstart + 258;

  /* Eight loop-invariant byte values.  All are read on every iteration
     of the outer do-loop.  On a register-starved target the allocator
     cannot keep all eight in registers and must spill at least one.  */
  u8 scan_end0 = scan[best_len];
  u8 scan_end1 = scan[best_len - 1];
  u8 scan_end2 = scan[best_len - 2];
  u8 scan_end3 = scan[best_len - 3];
  u8 scan_end4 = scan[best_len - 4];
  u8 scan_end5 = scan[best_len - 5];
  u8 scan_end6 = scan[best_len - 6];
  u8 scan_end7 = scan[best_len - 7];

  if (s->lookahead < 262)
    nice_match = s->lookahead;

  do {
    match = s->window + cur_match;

    /* All eight scan_end values are used here, on every iteration.  */
    if (match[best_len]     != scan_end0 ||
        match[best_len - 1] != scan_end1 ||
        match[best_len - 2] != scan_end2 ||
        match[best_len - 3] != scan_end3 ||
        match[best_len - 4] != scan_end4 ||
        match[best_len - 5] != scan_end5 ||
        match[best_len - 6] != scan_end6 ||
        match[best_len - 7] != scan_end7 ||
        *match              != *scan     ||
        *++match            != scan[1])
      continue;

    scan += 2;
    match++;
    do {
    } while (*++scan == *++match && *++scan == *++match &&
             *++scan == *++match && *++scan == *++match &&
             *++scan == *++match && *++scan == *++match &&
             *++scan == *++match && *++scan == *++match &&
             scan < strend);

    len = 258 - (int)(strend - scan);
    scan = strend - 258;

    if (len > best_len) {
      s->strstart = cur_match;
      best_len = len;
      if (len >= nice_match)
        break;
      /* Update all eight values; this branch is rarely taken, so the
         values are effectively invariants of the hot path.  */
      scan_end0 = scan[best_len];
      scan_end1 = scan[best_len - 1];
      scan_end2 = scan[best_len - 2];
      scan_end3 = scan[best_len - 3];
      scan_end4 = scan[best_len - 4];
      scan_end5 = scan[best_len - 5];
      scan_end6 = scan[best_len - 6];
      scan_end7 = scan[best_len - 7];
    }
  } while ((cur_match = prev[cur_match & wmask]) > limit
           && --chain_length != 0);

  return best_len;
}

Reply via email to