This is my magnum opus for GCC 17 (hopefully)
### Introduction
The main intended purpose of this work is threefold:
1. Support re-optimizing intrinsics and vector code in SLP that was created by
earlier vectorization passes. For example, two back-to-back loops could be
completely unrolled and vectorized during loop vectorization, but would not
then be optimized together. The idea is to let SLP discovery re-analyze the
vector code as a single entity. This makes the long-standing question of
whether BB SLP or loop vectorization should run first less important, since
we can re-vectorize the generated vector code.
2. Support Arm SVE2's Hybrid Vector Length Agnostic (HVLA) programming model.
The intention is to transform code written with intrinsics for older AdvSIMD
code into VLA code, so that we can use longer vector lengths.
3. Support workloads, particularly scientific-computing workloads, in which
users manually create vector types that are longer than the target supports
using GCC vector extensions. The expectation is that the compiler can then
decompose those operations into supported operations. Clang can do this for
some examples, although its approach seems quite limited and appears to work
only when the operations are constructors of PHI nodes.
4. Support vector decomposition and induction handling that were never valid for
the target but were writeable using GNU extensions.
To solve these problems, the approach I have taken in GCC is to model vector
types as possible base "scalar" types. The main advantage is that the current
compiler workflows can stay the same. Fundamentally, this does not change most
of the vectorizer, apart from analysis and the usual extensions to the code
generators.
This also means that the code remains subject to cost modelling and to other
features we have added over the years, such as epilogue masks.
As an example:
```c
#include <arm_neon.h>
void foo (uint8_t *a, uint8_t *b, uint8_t *c, int n)
{
for (int i = 0; i < n; i += 16, a += 16, b += 16, c += 16)
{
uint8x8_t av1 = vld1_u8 (a);
uint8x8_t av2 = vld1_u8 (a + 8);
uint8x8_t bv1 = vld1_u8 (b);
uint8x8_t bv2 = vld1_u8 (b + 8);
vst1_u8 (c, vadd_u8 (av1, bv1));
vst1_u8 (c + 8, vadd_u8 (av2, bv2));
}
}
```
At the moment, `-O3` generates:
```asm
.L3:
ldr d29, [x0, x4]
ldr d28, [x1, x4]
ldr d31, [x7, x4]
ldr d30, [x6, x4]
add v28.8b, v29.8b, v28.8b
add v30.8b, v31.8b, v30.8b
str d28, [x2, x4]
str d30, [x5, x4]
add x4, x4, 16
cmp w3, w4
bgt .L3
```
This underuses the available load bandwidth. Ideally these would be Q-sized
loads and one ADD, i.e. SLP would combine them.
With the branch, `-O3 -march=armv8-a` generates:
```asm
.L5:
ldr q31, [x0, x4]
ldr q30, [x1, x4]
add v30.16b, v31.16b, v30.16b
str q30, [x2, x4]
add x4, x4, 16
cmp x4, x7
bne .L5
```
`-O3 -march=armv8-a+sve` generates:
```asm
.L5:
ldr q31, [x0, x3]
ldr q30, [x1, x3]
add v30.16b, v31.16b, v30.16b
str q30, [x2, x3]
add x3, x3, 16
cmp x5, x3
bne .L5
lsl w3, w4, 4
cmp w6, w3
beq .L1
.L4:
sub w6, w6, w3
ubfiz x3, x3, 3, 32
whilelo p7.b, xzr, x6
ld1b z29.b, p7/z, [x0, x3]
ld1b z28.b, p7/z, [x1, x3]
add z28.b, z28.b, z29.b
st1b z28.b, p7, [x2, x3]
.L1:
```
`-O3 -march=armv8-a+sve -mautovec-preference=sve-only` generates:
```asm
.L4:
ld1b z31.b, p7/z, [x0, x4]
ld1b z30.b, p7/z, [x1, x4]
add z30.b, z30.b, z31.b
st1b z30.b, p7, [x2, x4]
add x4, x4, x5
whilelo p7.b, x4, x3
b.any .L4
```
### Representation
There is no change to the SLP trees themselves. The vector operation is added
to the node with N SLP lanes. Essentially every vector statement is of NUNITs
increases lane count by NUNIT lanes.
As temporary group matching for SLP builds the scalar statement is duplicated
but this is not the underlying representation used.
The group size for the operation is then set to the number of units in the
incoming vector. For memory accesses, `dr_group_size` is increased too. At
the moment, and probably for the first version, I only plan to support linear
loads and load-lane operations. Recognizing the other load types requires a
target hook and time to fill in the DRs.
For BB SLP, the current code starts a seed whenever it finds a store with a
vector type that the target does not support. This then fails analysis because
no vector type can be found. The plan is to make vector type selection return
the largest supported type for the given inner mode. During `vectorizable_*`
analysis, we would then avoid doing any other check beyond whether the target
supports the new mode for the given operation. To match the incoming VF, we
would increase the required unroll factor so that we get the same effective VF.
This patch series only supports linear, fixed length loads. This is because
modelling the other loads and VLA requires more representational changes. The
patch series also tries to weave in little by little tracking of vector mode
operations lane wise. So even though we have 1 vector, the implementation
tracks each individual lane. This is needed to get things like permutations
and inductions and reductions correct.
Lastly this patch will likely require target maintainers to add
-fno-tree-vectorize to some testcases in their targets. I have updated x86_64,
aarch64 and arm with this patch.
Others need to do this for themselves.
Decreasing the VF is not possible at the moment. For example:
```c
#include <arm_neon.h>
void foo (uint8_t *a, uint8_t *b, uint8_t *c, int n)
{
for (int i = 0; i < n; i += 32, a += 32, b += 32, c += 32)
{
uint8x16_t av1 = vld1q_u8 (a);
uint8x16_t av2 = vld1q_u8 (a + 16);
uint8x16_t bv1 = vld1q_u8 (b);
uint8x16_t bv2 = vld1q_u8 (b + 16);
vst1q_u8 (c, vaddq_u8 (av1, bv1));
vst1q_u8 (c + 16, vaddq_u8 (av2, bv2));
}
}
```
cannot collapse down to:
```asm
.L5:
ldr q31, [x0, x4]
ldr q30, [x1, x4]
add v30.16b, v31.16b, v30.16b
str q30, [x2, x4]
add x4, x4, 16
cmp x4, x7
bne .L5
```
This would require fractional VF support; in other words, it would require
support for re-rolling the loop. With the current design, that should work
naturally once we add fractional VF support.
### Areas that need the most changes
The biggest changes are in vector boolean handling, vector conditional
identification and SLP lane tracking.
The series is 14 long, only 1 of which are middle end code, and each patch
introduces an small isolated change to the vectorizer. I spent a lot of time
on making sure these are easy to review but some changes just needed to happen.
This also re-uses the IFN_MASK_CALL operation to support foldings from unmasked
to masked implementations. This required a changed to all targets which have a
custom folder implementation to reject this IFN. Hopefully a global reviewer
can approve this.
### Vector Operations
There are two types of vector operations that we want to support here.
The first type uses a normal GIMPLE representation. In the examples above,
`vadd` is lowered as a normal `PLUS_EXPR`, so testing and transforming it is
straightforward.
In many other cases, however, we see target intrinsics. To vectorize these, I
have added a new `vectorizable_vect_call` that uses a target hook,
`targetm.vectorize.translate_builtin_call`, to fill in vectorization
information about the call. In particular, the hook must be able to tell the
vectorizer whether we are going from an unmasked mode to a masked mode, whether
there is a builtin with masking support that we can use, and where the mask
argument appears in the argument list.
### Vector Booleans
Normally, the vectorizer has to detect booleans in order to differentiate data
operations from control operations. When vectorizing existing vector code,
that has already been done for us. However, we still need to be able to expand
the lanes and, if necessary, convert between boolean representation models.
For example, when changing AdvSIMD code into SVE code, the boolean
representation has to change because the underlying truth type is different.
To fix this and support boolean operations, there is a new pattern,
`vect_recog_vect_bool_pattern`, that runs only on boolean vector comparisons.
It transforms them using a pattern as a scalar boolean. That is, it creates:
```c
cmp ? -1 : 0
```
from a vector comparison, with the type of the operation set to the truth type
of the original code. This allows us to vectorize these as if they were normal
scalar comparisons.
For example:
```c
#include <arm_neon.h>
void foo (uint8_t *a, uint8_t *b, uint8_t *c, int n)
{
for (int i = 0; i < n; i += 8, a += 8, b += 8, c += 8)
{
uint8x8_t av1 = vld1_u8 (a);
uint8x8_t bv1 = vld1_u8 (b);
vst1_u8 (c, vcgt_u8 (av1, bv1));
}
}
```
compiled with `-march=armv9-a+sve -O3` gives:
```asm
.L5:
ldr q31, [x1, x3]
ldr q30, [x0, x3]
cmhi v30.16b, v30.16b, v31.16b
str q30, [x2, x3]
add x3, x3, 16
cmp x3, x5
bne .L5
lsl w3, w4, 4
cmp w6, w3
beq .L1
.L4:
sub w6, w6, w3
ptrue p6.b, all
ubfiz x3, x3, 3, 32
whilelo p7.b, xzr, x6
ld1b z28.b, p7/z, [x0, x3]
ld1b z27.b, p7/z, [x1, x3]
cmplo p15.b, p6/z, z27.b, z28.b
mov z29.b, p15/z, #-1
st1b z29.b, p7, [x2, x3]
.L1:
ret
.p2align 2,,3
.L6:
mov x4, 0
.p2align 5,,15
.L3:
ldr d26, [x0, x4]
ldr d25, [x1, x4]
cmhi v25.8b, v26.8b, v25.8b
str d25, [x2, x4]
add x4, x4, 8
cmp w3, w4
bgt .L3
ret
.L7:
mov w3, 0
b .L4
```
The vectorizer is now able to handle the conversion between different boolean
modes while preserving the lane values of the original code. The vectorizer
does this by tracking mask producing operations and mask consuming operations.
No target support is needed here so there are no additional hooks.
### Vector Patterns
The incoming code may use vector operations to perform masking through manually
if-converted code. For example:
```c
#include <arm_neon.h>
#include <arm_sve.h>
void foo (uint8_t *a, uint8_t *b, uint8_t *c, int n)
{
for (int i = 0; i < n; i += 8, a += 8, b += 8, c += 8)
{
uint8x8_t av1 = vld1_u8 (a);
uint8x8_t bv1 = vld1_u8 (b);
uint8x8_t cmp = vcgt_u8 (bv1, vdup_n_u8 (0));
uint8x8_t add = vadd_u8 (bv1, vdup_n_u8 (1));
uint8x8_t sel = vbsl_u8 (cmp, add, bv1);
vst1_u8 (a, sel);
}
}
```
This is a manually if-converted conditional add. We can re-vectorize it as-is,
but what we really want is to optimize it as a masked operation.
The vectorizer does this by tracking mask producing operations and mask
consuming operations. No target support is needed here so there are no
additional hooks.
In the above, the vectorizer tracks statements that produce masks, e.g. compares
and statements that consume masks, e.g. selects and tries to work out if
together the operation results in a conditional operation. This is needed
because operations like AArch64's BSL are bitwise operations and are only valid
usually when the producer has made a -1 or 0 values result.
The above for instance generates:
```asm
mov z30.b, #1
.L4:
ld1b z31.b, p7/z, [x1, x2]
cmpne p6.b, p7/z, z31.b, #0
add z31.b, p6/m, z31.b, z30.b
st1b z31.b, p7, [x0, x2]
incb x2
whilelo p7.b, x2, x3
b.any .L4
```
We can extend this with more idioms later as we find them.
### Vector decomposition
One of the primary use cases here is to also support vector decomposition to
known support vector operations.
This is handled by starting SLP from unsupported vector operations (or in SLP
terms, emulated vectors) and treating those as seeds.
For example
```
typedef float v4sf __attribute__((vector_size(16)));
typedef float v8sf __attribute__((vector_size(32)));
v8sf
swap_halves (v8sf v)
{
v4sf d[2];
v4sf t;
__builtin_memcpy (d, &v, sizeof v);
t = d[0];
d[0] = d[1];
d[1] = t;
__builtin_memcpy (&v, d, sizeof v);
return v;
}
```
today compiles to
```
swap_halves(float vector[8]):
ldp q31, q30, [x0]
sub sp, sp, #64
stp q30, q31, [sp]
ldr q29, [sp, 16]
stp q30, q29, [x8]
add sp, sp, 64
ret
```
but with revectorization compiles instead to
```
swap_halves:
ldp q30, q31, [x0]
stp q31, q30, [x8]
ret
```
Another example is
```
typedef unsigned vec256 __attribute__((__vector_size__(32)));
void vec_xor_shuf(vec256& x)
{
x ^= 1;
x = (vec256){x[4], x[0], x[5], x[1], x[6], x[2], x[7], x[3]};
}
```
which today produces:
```
vec_xor_shuf(unsigned int vector[8]&):
ldp q30, q31, [x0]
mov w3, 0
mov w4, 0
mov w1, 0
mov w2, 0
mov v29.16b, v31.16b
eor z30.s, z30.s, 1
eor z29.s, z29.s, 1
dup s28, v30.s[1]
dup s31, v30.s[2]
fmov x5, d29
dup s26, v29.s[1]
dup s27, v29.s[2]
dup s29, v29.s[3]
bfi x3, x5, 0, 32
fmov x5, d30
dup s30, v30.s[3]
bfi x3, x5, 32, 32
fmov x5, d26
bfi x4, x5, 0, 32
fmov x5, d27
bfi x1, x5, 0, 32
fmov x5, d29
bfi x2, x5, 0, 32
fmov x5, d28
bfi x4, x5, 32, 32
stp x3, x4, [x0]
fmov x3, d31
bfi x1, x3, 32, 32
fmov x3, d30
bfi x2, x3, 32, 32
stp x1, x2, [x0, 16]
ret
```
where vect lowering had no choice but to scalarize the operation.
With the patch this produces
```
```
_Z12vec_xor_shufRDv8_j:
ldp q31, q30, [x0]
eor z31.s, z31.s, 1
eor z30.s, z30.s, 1
zip1 v29.4s, v30.4s, v31.4s
zip2 v30.4s, v30.4s, v31.4s
stp q29, q30, [x0]
ret
```
I am still working through the PRs that this patch fixes so have only included
the main one. But believe we are in a good enough shape for stage 1.
For the final commit (if accepted) I will add all the PRs.
--