On 25/08/2026 14:30, Wilco Dijkstra wrote:
> 
> The existing Arm search_line_fast is ancient and not well optimized.
> Optimize it for typical inputs (short lines) - what matters is to quickly
> get the match result for the first few vectors with minimal initialization
> overhead.  We simply loop until a match is found even if the input pointer
> is unaligned or close to the end.  Since this may overread, it relies on
> *end containing a match and CPP_BUFFER_PADDING >= 16.
> 
> Lookup the low 3 bits of each character using 2 64-bit TBLs and compare
> the 128-bit result with the original input.  The lookup table contains
> the 4 search characters at entries MOD 8, so a match results in the same
> character.  This works because the low 3 bits of the search characters
> are unique.
> 
> The optimized sequence is less than a third of the old code:
> 
>         vmov.i8 q12, #7  @ v16qi
>         vldr    d22, .L29
> .L25:
>         vld1.8  {d18-d19}, [r0]!
>         vand    q10, q9, q12
>         vtbl.8  d16, {d22}, d20
>         vtbl.8  d17, {d22}, d21
>         vceq.i8 q8, q8, q9
>         vaddhn.i16      d16, q8, q8
>         vmov    r2, r3, d16     @ int
>         orrs    r1, r2, r3
>         beq     .L25
>         cmp     r2, #0
>         rbiteq  r2, r3
>         rbitne  r2, r2
>         clzeq   r2, r2
>         clzne   r2, r2
>         addeq   r2, r2, #32
>         sub     r2, r2, #61
>         add     r0, r0, r2, asr #2
>         bx      lr
> 
> Performance is significantly better as a result: on a huge trace that replays
> the calls from libcpp it is ~24% faster on Neoverse N1.
> 
> Passes bootstrap, OK for commit?
> 
> libcpp/ChangeLog:
> 
>         * lex.cc (search_line_fast): Optimized Arm implementation.

This is really, neat.  Thank you.  A couple of additional comments I would like 
adding, but otherwise this is OK.

> 
> ---
> 
> diff --git a/libcpp/lex.cc b/libcpp/lex.cc
> index 
> 0e4d29a2950a45b9e43f4673bf69394134264660..0c4e479ec4b847fd136a4c1950603d2ef69d6806
>  100644
> --- a/libcpp/lex.cc
> +++ b/libcpp/lex.cc
> @@ -785,64 +785,49 @@ search_line_fast (const uchar *s, const uchar *end)
>  #elif defined (__ARM_NEON)
>  #include "arm_neon.h"
>  
> -static const uchar *
> -search_line_fast (const uchar *s, const uchar *end ATTRIBUTE_UNUSED)
> -{
> -  const uint8x16_t repl_nl = vdupq_n_u8 ('\n');
> -  const uint8x16_t repl_cr = vdupq_n_u8 ('\r');
> -  const uint8x16_t repl_bs = vdupq_n_u8 ('\\');
> -  const uint8x16_t repl_qm = vdupq_n_u8 ('?');
> -  const uint8x16_t xmask = (uint8x16_t) vdupq_n_u64 (0x8040201008040201ULL);
> +/* A fast NEON scanner implementation using table lookup.
>  
> -  unsigned int misalign, found, mask;
> -  const uint8_t *p;
> -  uint8x16_t data;
> +   Lookup the low 3 bits of each character using 2 64-bit TBLs and compare
> +   the 128-bit result with the original input.  The lookup table contains
> +   the 4 search characters at entries MOD 8, so a match results in the same
> +   character.  This works because the low 3 bits of the search characters
> +   are unique.

Add a note that this assumes the ASCII encoding set.  I've never seen anything 
else used on Arm processors, but it's not beyond the bounds of possibility.
>  
> -  /* Align the source pointer.  */
> -  misalign = (uintptr_t)s & 15;
> -  p = (const uint8_t *)((uintptr_t)s & -16);
> -  data = vld1q_u8 (p);
> +   To get good performance, what matters is to quickly get the match result
> +   for the first few vectors with minimal initialization overhead.
> +   We simply loop until a match is found even if the input pointer is
> +   unaligned or close to the end.  Since this may overread, it relies on
> +   *end containing a match and CPP_BUFFER_PADDING >= 16.
> +*/
>  
> -  /* Create a mask for the bytes that are valid within the first
> -     16-byte block.  The Idea here is that the AND with the mask
> -     within the loop is "free", since we need some AND or TEST
> -     insn in order to set the flags for the branch anyway.  */
> -  mask = (-1u << misalign) & 0xffff;
> +static_assert (CPP_BUFFER_PADDING >= 16, "");
>  
> -  /* Main loop, processing 16 bytes at a time.  */
> -  goto start;
> +static const uchar *
> +search_line_fast (const uchar *s, const uchar *end ATTRIBUTE_UNUSED)
> +{
> +  uint8x16_t data, res, m;
> +  uint8x8_t tab, res1, res2;
> +  uint64_t mask;
> +  m = vdupq_n_u8 (0x7);
> +  tab = (uint8x8_t) vdup_n_u64 (0x3f000d5c000a0001UL);

There are two things that would make this a bit clearer:

Put the 'ascii' characters in a comment alongside the constant (perhaps move it 
to a #define)

/*  '?', 0, '\r', '\\', 0, '\n', 0, impossible.  */
#define BSL_NL_RET_QRY 0x3f000d5c000a0001UL

 
I think it would be worth a comment on exactly how this code works (it takes 
some thinking about).  Perhaps

/* The algorithm below performs

        (tab[data[lane]%8] == data[lane])

    in parallel for each character in the input string, which will be true iff
    the character is one we are looking for.  The 'impossible' encoding ensures
    we cannot match against a NUL byte (other 0 values are also impossible).  */

 >  
> -  do
> +  while (1)
>      {
> -      uint8x8_t l;
> -      uint16x4_t m;
> -      uint32x2_t n;
> -      uint8x16_t t, u, v, w;
> -
> -      p += 16;
> -      data = vld1q_u8 (p);
> -      mask = 0xffff;
> -
> -    start:
> -      t = vceqq_u8 (data, repl_nl);
> -      u = vceqq_u8 (data, repl_cr);
> -      v = vorrq_u8 (t, vceqq_u8 (data, repl_bs));
> -      w = vorrq_u8 (u, vceqq_u8 (data, repl_qm));
> -      t = vandq_u8 (vorrq_u8 (v, w), xmask);
> -      l = vpadd_u8 (vget_low_u8 (t), vget_high_u8 (t));
> -      m = vpaddl_u8 (l);
> -      n = vpaddl_u16 (m);
> -
> -      found = vget_lane_u32 ((uint32x2_t) vorr_u64 ((uint64x1_t) n,
> -           vshr_n_u64 ((uint64x1_t) n, 24)), 0);
> -      found &= mask;
> +      data = vld1q_u8 (s);
> +      s += 16;
> +      res = vandq_u8 (data, m);
> +      res1 = vtbl1_u8 (tab, vget_low_u8 (res));
> +      res2 = vtbl1_u8 (tab, vget_high_u8 (res));
> +      res = vcombine_u8 (res1, res2);
> +      res = vceqq_u8 (res, data);
> +      mask = (uint64_t) vaddhn_u16 ((uint16x8_t)res, (uint16x8_t)res);
> +      /* Use asm to ensure the vmov r2, r3, d16 to transfer 'mask' to scalar
> +      registers does not get split into 32-bit transfers.  */
> +      asm ("" : "+r" (mask));
> +      /* After CTZ adjust for ADDHN bias and the post-increment of 's'.  */
> +      if (mask)
> +     return s + ((__builtin_ctzll (mask) + 3 - 16 * 4) >> 2);
>      }
> -  while (!found);
> -
> -  /* FOUND contains 1 in bits for which we matched a relevant
> -     character.  Conversion to the byte index is trivial.  */
> -  found = __builtin_ctz (found);
> -  return (const uchar *)p + found;
>  }
>  
>  #else


R.

Reply via email to