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))) {