From 77774c78b28ec991ef50a3a91424ad418a00d036 Mon Sep 17 00:00:00 2001
From: Sergey Fedorov <barracuda@macos-powerpc.org>
Date: Tue, 11 Aug 2026 08:45:37 +0000
Subject: [PATCH 2/3] Add AltiVec nlcount() and isutf8()

nlcount() keeps the 16 byte alignment prologue that the SSE2 path already
uses, so every load is an aligned vec_ld and no vec_perm is needed at all.
vec_sum4s (vsum4ubs) accumulates four bytes into each 32 bit lane, so the
horizontal sum runs once per call instead of once per 64 byte block as it
must on SSE2, which needs eight pextrw there.  That is 16 instructions per
64 bytes against roughly 25 for SSE2.  Four independent accumulators are
used because chaining them serializes on the vsum4ubs latency.  A lane
saturates only after 17GB in a single call.

isutf8() is ported from the NEON version rather than the SSE2 one: NEON
vextq_s8(a,b,n) is exactly vec_sld(a,b,n) on big endian, so the cross lane
shifts map one to one, and vec_add(v,v) is the per byte shift left by one.
The step 2 test that every byte has bit 7 set becomes a single vec_all_lt
on signed bytes, replacing the SSE2 movemask compare and NEON's two lane
extracts.

Verified against a scalar build of the same functions over 32000 differential
cases covering stray continuation bytes, invalid 0xf5-0xff leads, overlong
0xc0/0xc1 leads, embedded NULs and truncated multibyte sequences at eight
alignments, plus 5186 nlcount cases against a scalar reference across six
newline densities, 32 alignments and 27 lengths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSbbKgkiRQmVj792gErwVD
---
 lib/simd.cpp | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 100 insertions(+)

diff --git a/lib/simd.cpp b/lib/simd.cpp
index 46d7c3b..88d8202 100644
--- a/lib/simd.cpp
+++ b/lib/simd.cpp
@@ -138,6 +138,46 @@ size_t nlcount(const char *s, const char *t)
       n += static_cast<uint8_t>(sum2) + (sum2 >> 8);
 #endif
     }
+#elif defined(HAVE_ALTIVEC)
+    {
+      const char *e = t - 64;
+      // align on 16 bytes: VMX has no unaligned vector load, aligned vec_ld() avoids a vec_perm() per load
+      while ((reinterpret_cast<std::ptrdiff_t>(s) & 0x0f) != 0)
+        n += (*s++ == '\n');
+      const __vector unsigned char vlcn = simd_set1_u8('\n');
+      const __vector unsigned char v0 = simd_set1_u8(0);
+      // vsum4ubs accumulates four bytes into each of the four 32 bit lanes, at most 16 per lane per
+      // iteration, so a single horizontal sum at the end suffices, unlike SSE2 which reduces per block
+      // (a lane saturates only after 17GB in one call, far beyond any buffer this is called with)
+      // four independent accumulators, chaining them would serialize on the vsum4ubs latency
+      __vector unsigned int vsum1 = vec_splats(0u);
+      __vector unsigned int vsum2 = vsum1;
+      __vector unsigned int vsum3 = vsum1;
+      __vector unsigned int vsum4 = vsum1;
+      while (s <= e)
+      {
+        const unsigned char *p = reinterpret_cast<const unsigned char*>(s);
+        __vector unsigned char vlcm1 = vec_ld(0, p);
+        __vector unsigned char vlcm2 = vec_ld(16, p);
+        __vector unsigned char vlcm3 = vec_ld(32, p);
+        __vector unsigned char vlcm4 = vec_ld(48, p);
+        // vec_cmpeq() sets 0xff per match, subtract from zero to get 0 or 1 per byte
+        // a C style cast reinterprets an AltiVec vector, static_cast does not and NEON uses vreinterpretq_*
+        __vector unsigned char vlceq1 = vec_sub(v0, (__vector unsigned char)vec_cmpeq(vlcm1, vlcn));
+        __vector unsigned char vlceq2 = vec_sub(v0, (__vector unsigned char)vec_cmpeq(vlcm2, vlcn));
+        __vector unsigned char vlceq3 = vec_sub(v0, (__vector unsigned char)vec_cmpeq(vlcm3, vlcn));
+        __vector unsigned char vlceq4 = vec_sub(v0, (__vector unsigned char)vec_cmpeq(vlcm4, vlcn));
+        vsum1 = vec_sum4s(vlceq1, vsum1);
+        vsum2 = vec_sum4s(vlceq2, vsum2);
+        vsum3 = vec_sum4s(vlceq3, vsum3);
+        vsum4 = vec_sum4s(vlceq4, vsum4);
+        s += 64;
+      }
+      // horizontal sum of the four lanes, endian neutral and only once per call
+      union { __vector unsigned int v; uint32_t u[4]; } vres;
+      vres.v = vec_add(vec_add(vsum1, vsum2), vec_add(vsum3, vsum4));
+      n += vres.u[0] + vres.u[1] + vres.u[2] + vres.u[3];
+    }
 #endif
   }
   // 4-way auto-vectorizable loop
@@ -398,6 +438,66 @@ bool isutf8(const char *s, const char *e)
       continue;
   }
 
+#elif defined(HAVE_ALTIVEC)
+
+  if (s <= e - 16)
+  {
+    // this mirrors the NEON method: AltiVec vec_sld() is the same cross lane byte shift as NEON vextq_s8()
+    // and AltiVec compares set CR6, so the whole vector tests below are a single record form compare
+    const __vector signed char v0 = vec_splats(static_cast<signed char>(0));
+    simd_ldu_t ctl = simd_ldu_init(s);
+    // prep step: scan ASCII first for speed, then check remaining UTF-8
+    while (s <= e - 16)
+    {
+      __vector signed char vc = (__vector signed char)simd_ldu(s, ctl);
+      if (!vec_all_gt(vc, v0))
+      {
+        // non-ASCII, return false if a NUL was found
+        if (vec_any_eq(vc, v0))
+          return false;
+        break;
+      }
+      s += 16;
+    }
+    // my UTF-8 check method, see the SSE2 and NEON code above for the scalar version and the derivation
+    const __vector signed char vxc0 = vec_splats(static_cast<signed char>(0xc0));
+    const __vector signed char vxc1 = vec_splats(static_cast<signed char>(0xc1));
+    const __vector signed char vxf5 = vec_splats(static_cast<signed char>(0xf5));
+    __vector signed char vp = v0;
+    __vector signed char vq = v0;
+    __vector signed char vr = v0;
+    ctl = simd_ldu_init(s);
+    while (s <= e - 16)
+    {
+      // step 1: check valid signed byte ranges, including continuation bytes 0x80 to 0xbf
+      __vector signed char vc = (__vector signed char)simd_ldu(s, ctl);
+      __vector signed char vt = vec_and((__vector signed char)vec_cmpgt(vc, vxc1), (__vector signed char)vec_cmplt(vc, vxf5));
+      vt = vec_or(vt, (__vector signed char)vec_cmplt(vc, vxc0));
+      vt = vec_or(vt, (__vector signed char)vec_cmpgt(vc, v0));
+      __vector signed char vm = vt;
+      // step 2: check UTF-8 multi-byte sequences of 2, 3 and 4 bytes long, shifting in the previous
+      // vp, vq and vr with vec_sld(), and vec_add(v, v) is the per byte shift left by 1
+      __vector signed char vo = vp;
+      vp = vec_and(vc, vec_add(vc, vc));
+      vt = vec_sld(vo, vp, 15);
+      vo = vq;
+      vq = vec_and(vp, vec_add(vp, vp));
+      vt = vec_or(vt, vec_sld(vo, vq, 14));
+      vo = vr;
+      vr = vec_and(vq, vec_add(vq, vq));
+      vt = vec_or(vt, vec_sld(vo, vr, 13));
+      vt = vec_xor(vt, (__vector signed char)vec_cmpgt(vc, vxc1));
+      vm = vec_and(vm, vt);
+      // every byte must have bit 7 set, i.e. must be negative as a signed byte: one vcmpgtsb. and a branch
+      if (!vec_all_lt(vm, v0))
+        return false;
+      s += 16;
+    }
+    // do not end in the middle of a UTF-8 multibyte sequence, backtrack when necessary (this will terminate)
+    while ((*--s & 0xc0) == 0x80)
+      continue;
+  }
+
 #endif
 
   while (s < e)
