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

            Bug ID: 126385
           Summary: aarch64: integer GEMV reduction vectorized as one
                    serial dot-product chain instead of parallel
                    accumulators
           Product: gcc
           Version: 17.0
            Status: UNCONFIRMED
          Keywords: missed-optimization
          Severity: normal
          Priority: P3
         Component: tree-optimization
          Assignee: unassigned at gcc dot gnu.org
          Reporter: konstantinos.eleftheriou at vrull dot eu
  Target Milestone: ---
            Target: aarch64-*-*

Created attachment 65122
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65122&action=edit
reduced_testcase

An integer matrix-vector multiply (the affine / GEMV dot product,
sum += weights[i][j] * input[j]) gets vectorized on the inner loop into a
single usdot accumulator. That accumulator is loop-carried, so the inner
loop runs at the usdot latency, one dot product deep, even though the
outer loop hands us plenty of independent output rows to work on at the
same time.

Compiling the testcase with `-O2 -mcpu=ampere1a -S reduced_testcase.c`
generates following for the inner loop:

    .L2:
        ldr   q30, [x1, x0]
        ldr   q29, [x3, x0]
        add   x0, x0, 16
        usdot v31.4s, v29.16b, v30.16b
        cmp   x0, 512
        bne   .L2

Every usdot reads and writes v31, so each iteration waits on the one
before it.

Since the output rows are independent, the way out is to run several
accumulators at once. Writing the same kernel to compute four output
rows per outer iteration, with four accumulators and one shared input
load, gets GCC to emit exactly that:

    for (int i = 0; i < OUT_DIM; i += 4)
      {
        int32_t s0 = biases[i+0], s1 = biases[i+1];
        int32_t s2 = biases[i+2], s3 = biases[i+3];
        const int8_t *w0 = weights + (i+0) * IN_DIM;
        const int8_t *w1 = weights + (i+1) * IN_DIM;
        const int8_t *w2 = weights + (i+2) * IN_DIM;
        const int8_t *w3 = weights + (i+3) * IN_DIM;
        for (int j = 0; j < IN_DIM; ++j)
          {
            s0 += (int32_t) w0[j] * (int32_t) input[j];
            s1 += (int32_t) w1[j] * (int32_t) input[j];
            s2 += (int32_t) w2[j] * (int32_t) input[j];
            s3 += (int32_t) w3[j] * (int32_t) input[j];
          }
        out[i+0] = s0; out[i+1] = s1;
        out[i+2] = s2; out[i+3] = s3;
      }

    .L2:
        ldr   q31, [x3, x1]
        ldr   q26, [x5, x1]
        usdot v30.4s, v31.16b, v26.16b
        ldr   q26, [x6, x1]
        usdot v29.4s, v31.16b, v26.16b
        ldr   q26, [x7, x1]
        usdot v27.4s, v31.16b, v26.16b
        ldr   q26, [x4, x1]
        add   x1, x1, 16
        usdot v28.4s, v31.16b, v26.16b
        cmp   x1, 512
        bne   .L2

Four independent accumulators (v30, v29, v27, v28) and one shared input
load, so the loop is no longer latency-bound.

So GCC can generate the parallel form, it just will not derive it from
the natural single-output source. Everything it needs is there:
independent output rows in the outer loop and a reduction over the inner
loop.

This shows up in SPEC2026's 706.stockfish_r benchmark.

Suggested fix:

Extend unroll-and-jam: strip-mine the outer loop by N and fuse
the N inner reductions into one body that keeps N independent
accumulator PHIs, rather than collapsing them into a single serial chain
the way plain jamming would. The vectorizer then sees one inner loop with N
independent reductions and emits N parallel dot-product accumulators.
  • [Bug tree-optimizati... konstantinos.eleftheriou at vrull dot eu via Gcc-bugs

Reply via email to