Tamar Christina <tamar.christ...@arm.com> writes: > Hi All, > > When a target does not support gathers and scatters the vectorizer tries to > emulate these using scalar loads/stores and a reconstruction of vectors from > scalar. > > The loads are still marked with VMAT_GATHER_SCATTER to indicate that they are > gather/scatters, however the vectorizer also asks the target to cost the > instruction that generates the indexes for the emulated instructions. > > This is done by asking the target to cost vec_to_scalar and vec_construct with > a stmt_vinfo being the VMAT_GATHER_SCATTER. > > Since Adv. SIMD does not have an LD1 variant that takes an Adv. SIMD Scalar > element the operation is lowered entirely into a sequence of GPR loads to > create > the x registers for the indexes. > > At the moment however we don't cost these, and so the vectorizer things that > when it emulates the instructions that it's much cheaper than using an actual > gather/scatter with SVE. Consider: > > #define iterations 100000 > #define LEN_1D 32000 > > float a[LEN_1D], b[LEN_1D]; > > float > s4115 (int *ip) > { > float sum = 0.; > for (int i = 0; i < LEN_1D; i++) > { > sum += a[i] * b[ip[i]]; > } > return sum; > } > > which before this patch with -mcpu=<sve-core> generates: > > .L2: > add x3, x0, x1 > ldrsw x4, [x0, x1] > ldrsw x6, [x3, 4] > ldpsw x3, x5, [x3, 8] > ldr s1, [x2, x4, lsl 2] > ldr s30, [x2, x6, lsl 2] > ldr s31, [x2, x5, lsl 2] > ldr s29, [x2, x3, lsl 2] > uzp1 v30.2s, v30.2s, v31.2s > ldr q31, [x7, x1] > add x1, x1, 16 > uzp1 v1.2s, v1.2s, v29.2s > zip1 v30.4s, v1.4s, v30.4s > fmla v0.4s, v31.4s, v30.4s > cmp x1, x8 > bne .L2 > > but during costing: > > a[i_18] 1 times vector_load costs 4 in body > *_4 1 times unaligned_load (misalign -1) costs 4 in body > b[_5] 4 times vec_to_scalar costs 32 in body > b[_5] 4 times scalar_load costs 16 in body > b[_5] 1 times vec_construct costs 3 in body > _1 * _6 1 times vector_stmt costs 2 in body > _7 + sum_16 1 times scalar_to_vec costs 4 in prologue > _7 + sum_16 1 times vector_stmt costs 2 in epilogue > _7 + sum_16 1 times vec_to_scalar costs 4 in epilogue > _7 + sum_16 1 times vector_stmt costs 2 in body > > Here we see that the latency for the vec_to_scalar is very high. We know the > intermediate vector isn't usable by the target ISA and will always be elided. > However these latencies need to remain high because when costing > gather/scatters > IFNs we still pass the nunits of the type along. In other words, the > vectorizer > is still costing vector gather/scatters as scalar load/stores. > > Lowering the cost for the emulated gathers would result in emulation being > seemingly cheaper. So while the emulated costs are very high, they need to be > higher than those for the IFN costing. > > i.e. the vectorizer generates: > > vect__5.9_8 = MEM <vector(4) intD.7> [(intD.7 *)vectp_ip.7_14]; > _35 = BIT_FIELD_REF <vect__5.9_8, 32, 0>; > _36 = (sizetype) _35; > _37 = _36 * 4; > _38 = _34 + _37; > _39 = (voidD.55 *) _38; > # VUSE <.MEM_10(D)> > _40 = MEM[(floatD.32 *)_39]; > > which after IVopts is: > > _63 = &MEM <vector(4) int> [(int *)ip_11(D) + ivtmp.19_27 * 1]; > _47 = BIT_FIELD_REF <MEM <vector(4) int> [(int *)_63], 32, 64>; > _41 = BIT_FIELD_REF <MEM <vector(4) int> [(int *)_63], 32, 32>; > _35 = BIT_FIELD_REF <MEM <vector(4) int> [(int *)_63], 32, 0>; > _53 = BIT_FIELD_REF <MEM <vector(4) int> [(int *)_63], 32, 96>; > > Which we correctly lower in RTL to individual loads to avoid the repeated > umov. > > As such, we should cost the vec_to_scalar as GPR loads and also do so for the > throughput which we at the moment cost as: > > note: Vector issue estimate: > note: load operations = 6 > note: store operations = 0 > note: general operations = 6 > note: reduction latency = 2 > note: estimated min cycles per iteration = 2.000000 > > Which means 3 loads for the GOR indexes are missing, making it seem like the > emulated loop has a much lower cycles per iter than it actually does since the > bottleneck on the load units are not modelled.
Yeah, currently the memory operations for an emulated 4-element load/store would be: - 1 vector load for the indices - 4 loads for the gather load, or 4 stores for the scatter store (and then +1 for the a[i] access in this case). Therefore... > But worse, because the vectorizer costs gathers/scatters IFNs as scalar > load/stores the number of loads required for an SVE gather is always much > higher than the equivalent emulated variant. > > Bootstrapped Regtested on aarch64-none-linux-gnu and no issues. > > Ok for master? > > Thanks, > Tamar > > gcc/ChangeLog: > > PR target/118188 > * config/aarch64/aarch64.cc (aarch64_vector_costs::count_ops): Adjust > throughput of emulated gather and scatters. > > gcc/testsuite/ChangeLog: > > PR target/118188 > * gcc.target/aarch64/sve/gather_load_12.c: New test. > > --- > > diff --git a/gcc/config/aarch64/aarch64.cc b/gcc/config/aarch64/aarch64.cc > index > 6bb4bdf2472e62d9b066a06561da8e516f1b3c3e..cb9b155826d12b622ae0df1736e4b042d01cf56a > 100644 > --- a/gcc/config/aarch64/aarch64.cc > +++ b/gcc/config/aarch64/aarch64.cc > @@ -17358,6 +17358,25 @@ aarch64_vector_costs::count_ops (unsigned int count, > vect_cost_for_stmt kind, > return; > } > > + /* Detect the case where we are using an emulated gather/scatter. When a > + target does not support gathers and scatters directly the vectorizer > + emulates these by constructing an index vector and then issuing an > + extraction for every lane in the vector. This is subsequently lowered > + by veclower into a series of loads which creates the scalar indexes for > + the subsequent loads. After the final loads are done it issues a > + vec_construct to recreate the vector from the scalar. For costing when > + we see a vec_to_scalar on a stmt with VMAT_GATHER_SCATTER we are dealing > + with an emulated instruction and should adjust costing properly. */ > + if (kind == vec_to_scalar > + && (m_vec_flags & VEC_ADVSIMD) > + && vect_mem_access_type (stmt_info, node) == VMAT_GATHER_SCATTER) > + { > + if (STMT_VINFO_TYPE (stmt_info) == load_vec_info_type) > + ops->loads += count - 1; > + else > + ops->stores += count - 1; ...since the aim is to replace: - 1 vector load for the indices with - 4 vector loads for the indices I think this should be changing ops->loads even for scatter stores. But this assumes that the gather load indices come directly from memory. That isn't always the case. If we have: float s4115 (int *ip) { float sum = 0.; for (int i = 0; i < LEN_1D; i++) { sum += a[i] * b[ip[i] + 1]; } return sum; } then we'll have a vector load, a vector add, and then the 4 umovs that, as you said above, were elided by post-vectoriser optimisations in your b[ip[i]] example: .L2: ldr q31, [x0, x1] ldr q29, [x6, x1] add x1, x1, 16 add v31.4s, v31.4s, v26.4s umov w5, v31.s[1] umov w4, v31.s[3] umov w3, v31.s[2] fmov w8, s31 ldr s30, [x2, w5, sxtw 2] ldr s27, [x2, w4, sxtw 2] ldr s31, [x2, w8, sxtw 2] ldr s28, [x2, w3, sxtw 2] uzp1 v30.2s, v30.2s, v27.2s uzp1 v31.2s, v31.2s, v28.2s zip1 v31.4s, v31.4s, v30.4s fmla v0.4s, v29.4s, v31.4s cmp x1, x7 bne .L2 These umovs are currently modelled: note: load operations = 6 note: store operations = 0 note: general operations = 7 note: reduction latency = 2 although I'm guessing it should be: note: load operations = 6 note: store operations = 0 note: general operations = 9 note: reduction latency = 2 instead. The underaccounting comes from vec_construct, which counts 1 rather than 3 operations for moving the scalars back to a vector. This patch would remove the umov accounting to give: note: load operations = 9 note: store operations = 0 note: general operations = 3 note: reduction latency = 2 Counting loads rather than general ops wouldn't matter for tight loops like these in which memory dominates anyway, since counting loads is then pessimistically correct. But in a loop that is compute bound, it's probably more important to get this right. So I think ideally, we should try to detect whether the indices come directly from memory or are the result of arithmetic. In the former case, we should do the loads adjustment above. In the latter case, we should keep the vec_to_scalar accounting unchanged. Of course, these umovs are likely to be more throughput-limited than we model, but that's a separate pre-existing problem... For the scatter store case: float s4115 (int *ip) { for (int i = 0; i < LEN_1D; i++) { b[ip[i]] = a[i] + 1; } } the vectoriser (unhelpfully) costs both the index-to-scalars and data-to-scalars as vec_to_scalar, meaning that we'll double-count the extra loads. Thanks, Richard > + return; > + } > > /* Count the basic operation cost associated with KIND. */ > switch (kind) > diff --git a/gcc/testsuite/gcc.target/aarch64/sve/gather_load_12.c > b/gcc/testsuite/gcc.target/aarch64/sve/gather_load_12.c > new file mode 100644 > index > 0000000000000000000000000000000000000000..d550f005d638f260973312fbccd05e2bb9aef1a7 > --- /dev/null > +++ b/gcc/testsuite/gcc.target/aarch64/sve/gather_load_12.c > @@ -0,0 +1,20 @@ > +/* { dg-do compile } */ > +/* { dg-additional-options "-Ofast -mcpu=neoverse-v2" } */ > + > +#define iterations 100000 > +#define LEN_1D 32000 > + > +float a[LEN_1D], b[LEN_1D]; > + > +float > +s4115 (int *ip) > +{ > + float sum = 0.; > + for (int i = 0; i < LEN_1D; i++) > + { > + sum += a[i] * b[ip[i]]; > + } > + return sum; > +} > + > +/* { dg-final { scan-assembler {\s+ld1w\t} } } */