On Tue, 23 Jun 2026, Richard Sandiford wrote:

> Alfie Richards <[email protected]> writes:
> > Hi All,
> >
> > This is my first attempt at an implementation of FFR.
> >
> > 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).
> >
> > 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 ();
> >   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.
> >
> > - 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.
> 
> Hmm, interesting.  I hadn't seen that approach being proposed before.
> I think the "classical" idea (is SVE old enough for things to be
> classical?) was that we would have two copies of the non-speculative
> part of the loop body: a full vector version for when no fault is
> detected and an FFR-predicated version for when FFR has zero bits.
> 
> The FFR-predicated version would advance by less than a full vector,
> which like you say would affect induction vectors.  But as Robin says,
> my understanding is that the .LEN code already supports that.

IIRC the .SELECT_VL path is constrainted by SLP:

      /* If any of the SLP instances cover more than a single lane
         we cannot use .SELECT_VL at the moment, even if the number
         of lanes is uniform throughout the SLP graph.  */
      if (LOOP_VINFO_USING_SELECT_VL_P (loop_vinfo))
        for (slp_instance inst : LOOP_VINFO_SLP_INSTANCES (loop_vinfo))
          if (SLP_TREE_LANES (SLP_INSTANCE_TREE (inst)) != 1
              && !(SLP_INSTANCE_KIND (inst) == slp_inst_kind_store
                   && SLP_INSTANCE_TREE (inst)->ldst_lanes))
            {
              LOOP_VINFO_USING_SELECT_VL_P (loop_vinfo) = false;
              break;
            }

which is I think exactly because of the need to swizzle invariants.
I'm not sure if we behave correctly with respect to alignment
with .SELECT_VL (but IIRC riscv only requires element alignment)
though we treat the IV step as variable in some places at least
when .SELECT_VL is used.

> (We would still be advancing by a whole number of scalar iterations,
> so invariant vectors shouldn't need to change.)
> 
> The supposed advantages of that were (IIRC):
> 
> (1) The full vector version would be the same as for a non-FFR loop.
>     That is, the overhead for the "good" case would be even lower
>     than above.
> 
> (2) It would reduce the number of vector iterations.
> 
> (3) In practice, a load would only be suppressed starting at index X if
>     X corresponds to a natural boundary (at least a cache boundary).
>     Restarting the loop at that point might make future iterations
>     "more aligned", which might be faster in its own right, but might
>     also reduce the risk of future false suppressed faults.
> 
> Like you say below, it would be better to avoid first-faulting loads
> if all pointers are well-aligned.  If we do that, (3) would only come
> into play for mutually misaligned pointers (or for -O2).  But even for
> mutually misaligned pointers, it could be better (or no worse) to be
> aligned to one pointer than be misaligned to all pointers.
> 
> The advantage of your approach is that it would scale well for multiple
> groups of FFR loads in the same loop.  We don't support that yet AIUI,
> but we could in future.

It also saves code size which is at least good for -O2.

Richard.

Reply via email to