On Tue, 23 Jun 2026, Alfie Richards wrote:

> Hi All,
> 
> This is my first attempt at an implementation of FFR.

I'm replying (late) to this mail before reading all the followup 
discussion.  Sorry if parts are answered already.

> I think this needs some background and explanation
> (probably should be a comment at some point)
> 
> == AArch64 first faulting read ==
> 
> AArch64 first faulting reads are safe to execute speculatively because the
> hardware is able to return less than a full vector read.
> 
> The loads (both "first faulting" (FF) and "non-faulting" (NF)) set a predicate
> register (first fault register) with a mask of loaded elements.
> 
> The FF variant will fault only if the first active element to be loaded
> causes a fault. This guarantees forwards progress.
> 
> The NF variant will never cause a fault, at the expense of possibly
> loading no elements.
> 
> Architecturally, these loads are guaranteed to never cause a fault unless
> it is a FF load and there is a fault for the first element.
> 
> However, the hardware can also do partial loads whenever it wants.
> For instance this could happen at page boundaries or cache faults.
> 
> This means an FFR vectorized loop needs to be able to recover from partial
> loads back to vectoried code.
> (this seems to not be the case for riscv's equivalent feature).

Alternatively such "bad" hardware could be forced to fall to a scalar
epilog.

> Also of note, the first fault register starts as all 1's, and each subsequent
> load updates it if they fail to load any elements by diabling the bits of
> elements not loaded and all bits afterwards.
> 
> For instance, it may start as
> [1,1,1,1,1,1,1,1]
> then a load is executed and could change that to
> [1,1,1,1,1,1,0,0]
> 
> But then the next load, even if it loads all elements, will not re-enable
> the last two bits.
> 
> Additionally, you never get a state like
> [1,1,1,1,1,0,0,1]
> where there are inactive elements followed by an active element.
> 
> == Vectorized code structure ==
> 
> The use case currently is for loops with mutually misaligned pointers, both
> of which require safe speculative reads, where we cannot peel for alignment, 
> or
> would not want to incur the code size cost of versioning to check if we can
> peel.
> 
> For aarch64 this is only for early break as there aren't any other situations
> where we need safe speculative reads.
> 
> Then for a loop such as 
> 
> int
> foo_no_vect (uint16_t *const restrict src1,
>                  uint16_t *const restrict src2,
>                  unsigned int n)
> {
>   for (int i = 0; i < n; i++)
>     {
>       uint16_t v1 = src1[i];
>       uint16_t v2 = src2[i];
>       if (v1 + v2 == 0)
>         return 1;
>     }
>   return 0;
> }
> 
> We generate the following:
> 
> GIMPLE (after dce):
> 
>   _78 = max_mask_75 & _77;
>   ffr_preservation_82 = .READ_FAULT_STATE ();
>   .SET_FAULT_STATE ({ -1, ... });
> 
>   <bb 3> [local count: 1014686025]:
>   # vectp_src1.7_45 = PHI <vectp_src1.7_46(7), vectp_src1.8_41(13)>
>   # vectp_src2.11_60 = PHI <vectp_src2.11_61(7), vectp_src2.12_56(13)>
>   # ivtmp_72 = PHI <ivtmp_73(7), 0(13)>
>   # loop_mask_48 = PHI <next_mask_ffr_81(7), _78(13)>
>   vect_v1_14.9_49 = .MASK_FIRSTFAULT_LOAD (vectp_src1.7_45, 64B, 
> loop_mask_48, { 0, ... });
>   vect__5.10_55 = (vector([4,4]) int) vect_v1_14.9_49;
>   vect_v2_16.13_63 = .MASK_FIRSTFAULT_LOAD (vectp_src2.11_60, 64B, 
> loop_mask_48, { 0, ... });
>   vect__6.14_65 = (vector([4,4]) int) vect_v2_16.13_63;
>   ffr_mask_83 = .READ_FAULT_STATE ();

There seems to be no data dependence between .MASK_FIRSTFAULT_LOAD
and .READ_FAULT_STATE which IMO is bad design (see all the issues
we have with FPU state accesses).  You supposedly do not want to
have a dependence (due to the FFR) between the two .MASK_FIRSTFAULT_LOAD
so an extra inout argument to that isn't intended.  So the
alternative is to have separate virtual outputs and have
.READ_FAULT_STATE merge them via having N inputs?

>   if (ffr_mask_83 == { -1, ... })
>     goto <bb 14>; [99.95%]
>   else
>     goto <bb 15>; [0.05%]
> 
>   <bb 15> [local count: 10146860]:
>   .SET_FAULT_STATE ({ -1, ... });
>   _84 = ~ffr_mask_83;
>   ffr_loop_mask_85 = loop_mask_48 & ffr_mask_83;
> 
>   <bb 14> [local count: 1014686025]:
>   # ffr_loop_mask_68 = PHI <loop_mask_48(3), ffr_loop_mask_85(15)>
>   # ffr_num_iters_36 = PHI <POLY_INT_CST [4, 4](3), 0(15)>
>   # next_mask_ffr_80 = PHI <{ -1, ... }(3), _84(15)>
>   vect__7.15_66 = vect__5.10_55 + vect__6.14_65;
>     mask_patt_28.16_67 = vect__7.15_66 == { 0, ... };
>   vec_mask_and_69 = mask_patt_28.16_67 & ffr_loop_mask_68;
>   if (vec_mask_and_69 != { 0, ... })
>     goto <bb 9>; [5.50%]
>   else
>     goto <bb 4>; [94.50%]
> 
>   <bb 9> [local count: 55807731]:
>   .SET_FAULT_STATE (ffr_preservation_82);
>   goto <bb 5>; [100.00%]
> 
>   <bb 4> [local count: 958878295]:
>   _47 = ffr_num_iters_36 * 2;
>   vectp_src1.7_46 = vectp_src1.7_45 + _47;
>   vectp_src2.11_61 = vectp_src2.11_60 + _47;
>   ivtmp_73 = ivtmp_72 + ffr_num_iters_36;
>   next_mask_79 = .WHILE_ULT (ivtmp_73, _74, { 0, ... });
>   next_mask_ffr_81 = next_mask_79 & next_mask_ffr_80;
>   if (next_mask_ffr_81 != { 0, ... })
>     goto <bb 7>; [94.50%]
>   else
>     goto <bb 12>; [5.50%]
> 
>   <bb 12> [local count: 52738306]:
>   .SET_FAULT_STATE (ffr_preservation_82);
>   goto <bb 5>; [100.00%]
> 
>   <bb 7> [local count: 906139989]:
>   goto <bb 3>; [100.00%]
> 
>   <bb 5> [local count: 114863531]:
>   # _10 = PHI <1(9), 0(12), 0(2)>
>   return _10;
> 
> Or final assembly:
> 
> .L5:
>       add     x4, x4, x3
>       whilelo p7.s, x4, x2
>       add     x0, x0, x3, lsl 1
>       and     p7.b, p7/z, p14.b, p14.b
>       add     x1, x1, x3, lsl 1
>       ptest   p15, p7.b
>       b.none  .L7
> .L6:
>       ldff1h  z31.s, p7/z, [x0]
>       ldff1h  z30.s, p7/z, [x1]
>       cntw    x3
>       rdffr   p14.b
>       nots    p13.b, p15/z, p14.b
>       b.any   .L11
> .L4:
>       add     z31.s, z31.s, z30.s
>       cmpeq   p7.s, p7/z, z31.s, #0
>       b.none  .L5
>       mov     w0, 1
>       ret
> 
> == Notes ==
> 
> - When there is a "partial read", instead of treating that as a partial 
> iteration
>   and advancing by the number of loaded elements, we intead advance by 0
>   iterations ard repeat the same iteration with the previously processed
>   elements masked out. This preserves alignment with the starting position
>   and avoids having to do anything awkward such as possibly rotating
>   invariant vectors.

So the advantage is that we never need a scalar fallback?  But ISTR
there's other reasons why we might need that for early break still?

> - This prioritises the "good" case, by trying to keep
>   the "full read" path as tight as possible, and adding
>   a fixup branch to handle the case where there is a partial read.
> 
> - We preserve the state of the FFR register over the vectoried loop.
>   This is to prevent code written with intrinsics breaking by clobbering
>   their value in the FFR register. However, as the FFR is not preserved by
>   the AAPCS, this is nearly always optimized out.
> 
> - One downside of this approach is I dont think it will translate cleanly for
>   len based loop vectorization (riscv). However, as I understand it, the
>   riscv equivalent feature never does partial loads unless there is a genuine
>   fault, so we will not need to worry about recovering back to
>   vectorized code after a partial read (as it will either take the early break
>   or the fault). So no fixup should be needed and can use a subset of this.
> 
> == Remaining work to do ==
> 
> - Versioning
>   As FFR introduces overhead, it will always be slower than a mutually
>   aligned loop. So my ideal code generation for a vector with two pointers
>   requiring safe speculative reads is:
> 
>   At O2, where we do not want to incur the code size cost of versioning,
>   instead just use FFR to vectorize the loop (if profitable).
> 
>   At O3, version to create a mutually aligned non-FFR case and a (current 
> code gen)
>   if the pointers are mutually misaligned.
> 
>   This "FFR versioning" is not yet implemented.
> 
> - Costing
>   I haven't done anything to cost FFR yet, we will want accurate costing of 
> FFR
>   vs scalar and vs non-FFR vectorized to make the decisions on versioning and
>   what to use. 
> 
> - Optimization of the generated code
>   The generated code has room for improvement, primarily using rdffrs would
>   be a performance gain, and reducing some of the moves within the hot
>   section of the loop.
> 
> == Feedback wanted ==
> 
> - Overall design of this
> - What to call this within GCC (it's not really "first fault reads", maybe
>   "hardware safe speculative reads" (HSSR)?)
> - How we can sensibly cost this to allow the choice to do scalar over
>   FFR.

When a load is partial you need to consider the FFR for all computations
that require masking by loop masking.  Your example is simplified
to not have any, but consider a division - you show an else value
of zero, so before any use of loop_mask on an operation that is
dependent on the FFR affecting load value you need to read the FFR
and combine it with the loop mask, right?

> Sorry for the essay!

No need to sorry, it's very helpful.

ISTR AVX10.x introduces some non-faulting loads but I have to check.

Richard.

> 
> Bootstrapped and reg tested for aarch64, x68-64, arm32hf, riscv.
> 
> I also ran vect.exp with --param=vect-ffr-usage=2 and -msve-vector-bits=128
> with no errors.
> 
> Thoughts?
> 
> King regards,
> Alfie
> 
> Alfie Richards (8):
>   vect: Add internal functions and optabs for first fault loads
>   vect: Add SLP_NODE argument to vect_get_loop_mask.
>   vect: Make vect_maybe_permute_loop_masks optional and retargetable
>   vect: Add EXCLUDE_VIRTUALS argument to _slp_tree::push_vec_def.
>   vect: Add vect-ffr-usage param.
>   vect: Add FFR analysis
>   vect: Add FFR transformation
>   vect: Enable FFR
> 
>  .../aarch64/aarch64-sve-builtins-base.cc      |  78 +++--
>  gcc/config/aarch64/aarch64-sve-builtins.cc    |  24 ++
>  gcc/config/aarch64/aarch64-sve-builtins.h     |   1 +
>  gcc/config/aarch64/aarch64-sve.md             | 201 +++++++++--
>  gcc/config/aarch64/aarch64.cc                 |  10 +
>  gcc/config/aarch64/iterators.md               |   2 +
>  gcc/internal-fn.cc                            |  24 ++
>  gcc/internal-fn.def                           |  18 +
>  gcc/optabs-tree.cc                            |  57 +++
>  gcc/optabs-tree.h                             |   2 +
>  gcc/optabs.def                                |   5 +
>  gcc/params.opt                                |   4 +
>  gcc/testsuite/gcc.target/aarch64/sve/ffr_1.c  |  16 +
>  gcc/testsuite/gcc.target/aarch64/sve/ffr_2.c  |  35 ++
>  gcc/testsuite/gcc.target/aarch64/sve/ffr_3.c  |  42 +++
>  gcc/testsuite/gcc.target/aarch64/sve/ffr_4.c  |  25 ++
>  gcc/testsuite/gcc.target/aarch64/sve/ffr_5.c  |  25 ++
>  gcc/testsuite/gcc.target/aarch64/sve/ffr_6.c  |  43 +++
>  .../gcc.target/aarch64/sve/ffr_6_run.c        |  81 +++++
>  gcc/testsuite/gcc.target/aarch64/sve/ffr_7.c  |  16 +
>  gcc/testsuite/gcc.target/aarch64/sve/ffr_8.c  |  16 +
>  gcc/testsuite/gcc.target/aarch64/sve/ffr_9.c  |  18 +
>  .../gcc.target/aarch64/sve/noeffect11.c       |   2 +-
>  .../gcc.target/aarch64/sve/peel_ind_12.c      |   2 +-
>  .../gcc.target/aarch64/sve/peel_ind_12_run.c  |   2 +-
>  .../gcc.target/aarch64/sve/pfalse-load.c      |   6 +-
>  gcc/tree-data-ref.cc                          |   4 +
>  gcc/tree-ssa-alias.cc                         |   2 +
>  gcc/tree-ssa-loop-ivopts.cc                   |   2 +
>  gcc/tree-vect-data-refs.cc                    |   3 +-
>  gcc/tree-vect-loop-manip.cc                   | 328 +++++++++++++++++-
>  gcc/tree-vect-loop.cc                         | 264 +++++++++++++-
>  gcc/tree-vect-slp.cc                          |  32 +-
>  gcc/tree-vect-stmts.cc                        | 165 +++++++--
>  gcc/tree-vectorizer.h                         |  63 +++-
>  35 files changed, 1496 insertions(+), 122 deletions(-)
>  create mode 100644 gcc/testsuite/gcc.target/aarch64/sve/ffr_1.c
>  create mode 100644 gcc/testsuite/gcc.target/aarch64/sve/ffr_2.c
>  create mode 100644 gcc/testsuite/gcc.target/aarch64/sve/ffr_3.c
>  create mode 100644 gcc/testsuite/gcc.target/aarch64/sve/ffr_4.c
>  create mode 100644 gcc/testsuite/gcc.target/aarch64/sve/ffr_5.c
>  create mode 100644 gcc/testsuite/gcc.target/aarch64/sve/ffr_6.c
>  create mode 100644 gcc/testsuite/gcc.target/aarch64/sve/ffr_6_run.c
>  create mode 100644 gcc/testsuite/gcc.target/aarch64/sve/ffr_7.c
>  create mode 100644 gcc/testsuite/gcc.target/aarch64/sve/ffr_8.c
>  create mode 100644 gcc/testsuite/gcc.target/aarch64/sve/ffr_9.c
> 
> 

-- 
Richard Biener <[email protected]>
SUSE Software Solutions Germany GmbH,
Frankenstrasse 146, 90461 Nuernberg, Germany;
GF: Jochen Jaser, Andrew McDonald, Werner Knoblich; (HRB 36809, AG Nuernberg)

Reply via email to