From 1bab98e441ddd8618cb91b25890c562ddf4a38a5 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 12 Oct 2026 10:50:20 -0400 Subject: [PATCH] Use SSE2, not SSSE3 Pre-compilation could potentially enable SSSE3, when the target the code is being installed on might not actually have it. In order to use more modern instruction sets, we need runtime detection. It's simpler just to rely on the baseline for now. --- include/prism/compiler/accel.h | 9 ++++-- include/prism/internal/parser.h | 2 +- src/prism.c | 6 ++-- src/strpbrk.c | 50 +++++++++++++++++++-------------- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/include/prism/compiler/accel.h b/include/prism/compiler/accel.h index be23236d1d..a0cfed0dc7 100644 --- a/include/prism/compiler/accel.h +++ b/include/prism/compiler/accel.h @@ -7,11 +7,16 @@ /** * Platform detection for SIMD/fast-path implementations. At most one of these * macros is defined, selecting the best available vectorization strategy. + * + * Selection is made at compile time with no runtime CPU detection, and + * precompiled binaries run on machines other than the one that built them. So + * each strategy relies only on instructions that the architecture baseline + * guarantees: NEON is mandatory on AArch64 and SSE2 is mandatory on x86-64. */ #if (defined(__aarch64__) && defined(__ARM_NEON)) || (defined(_MSC_VER) && defined(_M_ARM64)) # define PRISM_HAS_NEON -#elif (defined(__x86_64__) && defined(__SSSE3__)) || (defined(_MSC_VER) && defined(_M_X64)) -# define PRISM_HAS_SSSE3 +#elif (defined(__x86_64__) && defined(__SSE2__)) || (defined(_MSC_VER) && defined(_M_X64)) +# define PRISM_HAS_SSE2 #elif defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ # define PRISM_HAS_SWAR #endif diff --git a/include/prism/internal/parser.h b/include/prism/internal/parser.h index 8dc9af18c4..ad12d936cd 100644 --- a/include/prism/internal/parser.h +++ b/include/prism/internal/parser.h @@ -933,7 +933,7 @@ struct pm_parser_t { */ bool warn_mismatched_indentation; -#if defined(PRISM_HAS_NEON) || defined(PRISM_HAS_SSSE3) || defined(PRISM_HAS_SWAR) +#if defined(PRISM_HAS_NEON) || defined(PRISM_HAS_SSE2) || defined(PRISM_HAS_SWAR) /* * Cached lookup tables for pm_strpbrk's SIMD fast path. Avoids rebuilding * the nibble-based tables on every call when the charset hasn't changed diff --git a/src/prism.c b/src/prism.c index 9bf07856cb..2e2811d216 100644 --- a/src/prism.c +++ b/src/prism.c @@ -1891,7 +1891,7 @@ char_is_identifier_utf8(const uint8_t *b, ptrdiff_t n) { * Up to three optimized implementations are selected at compile time, with a * no-op fallback for unsupported platforms: * 1. NEON — processes 16 bytes per iteration on aarch64. - * 2. SSSE3 — processes 16 bytes per iteration on x86-64. + * 2. SSE2 — processes 16 bytes per iteration on x86-64. * 3. SWAR — little-endian fallback, processes 8 bytes per iteration. */ @@ -1949,8 +1949,8 @@ scan_identifier_ascii(const uint8_t *start, const uint8_t *end) { return (size_t) (cursor - start); } -#elif defined(PRISM_HAS_SSSE3) -#include +#elif defined(PRISM_HAS_SSE2) +#include static PRISM_INLINE size_t scan_identifier_ascii(const uint8_t *start, const uint8_t *end) { diff --git a/src/strpbrk.c b/src/strpbrk.c index 383707eb72..70d5e5a64f 100644 --- a/src/strpbrk.c +++ b/src/strpbrk.c @@ -54,11 +54,11 @@ pm_strpbrk_explicit_encoding_set(pm_parser_t *parser, uint32_t start, uint32_t l * Up to three optimized implementations are selected at compile time, with a * no-op fallback for unsupported platforms: * 1. NEON — processes 16 bytes per iteration on aarch64. - * 2. SSSE3 — processes 16 bytes per iteration on x86-64. + * 2. SSE2 — processes 16 bytes per iteration on x86-64. * 3. SWAR — little-endian fallback, processes 8 bytes per iteration. */ -#if defined(PRISM_HAS_NEON) || defined(PRISM_HAS_SSSE3) || defined(PRISM_HAS_SWAR) +#if defined(PRISM_HAS_NEON) || defined(PRISM_HAS_SSE2) || defined(PRISM_HAS_SWAR) /** * Update the cached strpbrk lookup tables if the charset has changed. The @@ -67,7 +67,7 @@ pm_strpbrk_explicit_encoding_set(pm_parser_t *parser, uint32_t start, uint32_t l * skip table construction entirely. * * Builds three structures: - * - low_lut/high_lut: nibble-based lookup tables for SIMD matching (NEON/SSSE3) + * - low_lut/high_lut: nibble-based lookup tables for SIMD matching (NEON) * - table: 256-bit bitmap for scalar fallback matching (all platforms) */ static PRISM_INLINE void @@ -159,46 +159,54 @@ scan_strpbrk_ascii(pm_parser_t *parser, const uint8_t *source, size_t maximum, c return false; } -#elif defined(PRISM_HAS_SSSE3) -#include +#elif defined(PRISM_HAS_SSE2) +#include static PRISM_INLINE bool scan_strpbrk_ascii(pm_parser_t *parser, const uint8_t *source, size_t maximum, const uint8_t *charset, size_t *index) { pm_strpbrk_cache_update(parser, charset); - __m128i low_lut = _mm_loadu_si128((const __m128i *) parser->strpbrk_cache.low_lut); - __m128i high_lut = _mm_loadu_si128((const __m128i *) parser->strpbrk_cache.high_lut); - __m128i mask_0f = _mm_set1_epi8(0x0F); + /* + * SSE2 has no byte shuffle, so the nibble lookup used by NEON is not + * available. Instead, each charset byte is broadcast into its own vector + * and compared against the input directly. Charsets are short, so this is + * a handful of compares per 16 bytes. + */ + __m128i needles[sizeof(parser->strpbrk_cache.charset)]; + size_t needles_size = 0; + + for (size_t i = 0; i < sizeof(parser->strpbrk_cache.charset) && parser->strpbrk_cache.charset[i] != '\0'; i++) { + needles[needles_size++] = _mm_set1_epi8((char) parser->strpbrk_cache.charset[i]); + } + const __m128i zero = _mm_setzero_si128(); size_t idx = 0; while (idx + 16 <= maximum) { __m128i v = _mm_loadu_si128((const __m128i *) (source + idx)); - // If any byte has the high bit set, stop. + /* If any byte has the high bit set, stop. */ if (_mm_movemask_epi8(v) != 0) break; - // Nibble-based classification using pshufb (SSSE3), same as NEON - // vqtbl1q_u8. A byte matches iff (low_lut[lo_nib] & high_lut[hi_nib]) != 0. - __m128i lo_class = _mm_shuffle_epi8(low_lut, _mm_and_si128(v, mask_0f)); - __m128i hi_class = _mm_shuffle_epi8(high_lut, _mm_and_si128(_mm_srli_epi16(v, 4), mask_0f)); - __m128i matched = _mm_and_si128(lo_class, hi_class); + /* NUL is always a breakpoint, matching pm_strpbrk_cache_update. */ + __m128i matched = _mm_cmpeq_epi8(v, zero); + for (size_t i = 0; i < needles_size; i++) { + matched = _mm_or_si128(matched, _mm_cmpeq_epi8(v, needles[i])); + } - // Check if any byte matched. - int mask = _mm_movemask_epi8(_mm_cmpeq_epi8(matched, _mm_setzero_si128())); + int mask = _mm_movemask_epi8(matched); - if (mask == 0xFFFF) { - // All bytes were zero — no match in this chunk. + if (mask == 0) { idx += 16; continue; } - // Find the first matching byte (first non-zero in matched). - *index = idx + pm_ctzll((uint64_t) (~mask & 0xFFFF)); + /* Find the first matching byte. */ + *index = idx + pm_ctzll((uint64_t) (mask & 0xFFFF)); return true; } - // Scalar tail. + /* Scalar tail. */ while (idx < maximum && source[idx] < 0x80) { uint8_t byte = source[idx]; if (parser->strpbrk_cache.table[byte >> 6] & ((uint64_t) 1 << (byte & 0x3F))) {