From 87862dfcb8cd1fe6b59daa385b153d468704bdb9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 18:50:46 +0000 Subject: [PATCH 1/4] perf: use SIMD for ASCII scan, lowercase, and UTF transcoding Apply SSE2/NEON (SWAR fallback) to the IDNA hot paths: is_ascii, ascii_map, forbidden-domain checks, label-dot scan, and UTF-8/UTF-32 ASCII widen/narrow. Keep scalar tails for short strings and big-endian targets such as s390x. Co-authored-by: Yagiz Nizipli --- include/ada/idna/to_ascii.h | 6 +- src/mapping.cpp | 24 +-- src/simd.hpp | 400 ++++++++++++++++++++++++++++++++++++ src/to_ascii.cpp | 123 ++++++++--- src/unicode_transcoding.cpp | 49 ++--- tests/mapping_tests.cpp | 24 +++ tests/safety_tests.cpp | 117 +++++++++++ 7 files changed, 654 insertions(+), 89 deletions(-) create mode 100644 src/simd.hpp diff --git a/include/ada/idna/to_ascii.h b/include/ada/idna/to_ascii.h index 2eb46d5..d1acf81 100644 --- a/include/ada/idna/to_ascii.h +++ b/include/ada/idna/to_ascii.h @@ -28,8 +28,10 @@ std::string to_ascii(std::string_view ut8_string); // https://url.spec.whatwg.org/#forbidden-domain-code-point bool contains_forbidden_domain_code_point(std::string_view ascii_string); -bool constexpr is_ascii(std::u32string_view view); -bool constexpr is_ascii(std::string_view view); +// Runtime SIMD (SSE2/NEON) with a SWAR fallback. Not constexpr: vector +// paths cannot be evaluated at compile time. +bool is_ascii(std::u32string_view view) noexcept; +bool is_ascii(std::string_view view) noexcept; } // namespace ada::idna diff --git a/src/mapping.cpp b/src/mapping.cpp index f72f215..feae9e7 100644 --- a/src/mapping.cpp +++ b/src/mapping.cpp @@ -6,6 +6,7 @@ #include #include "table_store.hpp" +#include "simd.hpp" #include "mapping_tables.cpp" namespace ada::idna { @@ -81,28 +82,7 @@ static size_t utf8_count_codepoints(const uint8_t* ptr) noexcept { // --- ASCII fast path --------------------------------------------------------- void ascii_map(char* input, size_t length) { - auto broadcast = [](uint8_t v) -> uint64_t { - return 0x101010101010101ull * v; - }; - uint64_t broadcast_80 = broadcast(0x80); - uint64_t broadcast_Ap = broadcast(128 - 'A'); - uint64_t broadcast_Zp = broadcast(128 - 'Z' - 1); - size_t i = 0; - - for (; i + 7 < length; i += 8) { - uint64_t word{}; - std::memcpy(&word, input + i, sizeof(word)); - word ^= - (((word + broadcast_Ap) ^ (word + broadcast_Zp)) & broadcast_80) >> 2; - std::memcpy(input + i, &word, sizeof(word)); - } - if (i < length) { - uint64_t word{}; - std::memcpy(&word, input + i, length - i); - word ^= - (((word + broadcast_Ap) ^ (word + broadcast_Zp)) & broadcast_80) >> 2; - std::memcpy(input + i, &word, length - i); - } + simd::ascii_lowercase(input, length); } // Two-pass map: first validate + exact size, then write once (no growth diff --git a/src/simd.hpp b/src/simd.hpp new file mode 100644 index 0000000..8a66765 --- /dev/null +++ b/src/simd.hpp @@ -0,0 +1,400 @@ +#ifndef ADA_IDNA_SIMD_HPP +#define ADA_IDNA_SIMD_HPP + +// Portable SIMD helpers for IDNA hot paths (ASCII scan, lowercase, UTF +// widen/narrow). SSE2 is baseline on x86_64; NEON is baseline on aarch64. +// Other targets (s390x, RV64 without V, ...) use SWAR / scalar fallbacks. + +#include +#include +#include + +#if defined(__SSE2__) || defined(__x86_64__) || defined(__x86_64) || \ + defined(_M_AMD64) || defined(_M_X64) || \ + (defined(_M_IX86_FP) && _M_IX86_FP == 2) +#define ADA_IDNA_SSE2 1 +#include +#endif + +#if defined(__aarch64__) || defined(_M_ARM64) +#define ADA_IDNA_NEON 1 +#include +#endif + +#if defined(_MSC_VER) && !defined(__clang__) +#define ADA_IDNA_REALLY_INLINE __forceinline +#else +#define ADA_IDNA_REALLY_INLINE inline __attribute__((always_inline)) +#endif + +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) +#include +#endif + +namespace ada::idna::simd { + +#if defined(ADA_IDNA_SSE2) +ADA_IDNA_REALLY_INLINE int popcount_u32(unsigned v) noexcept { +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) + return static_cast(__popcnt(v)); +#elif defined(__GNUC__) || defined(__clang__) + return __builtin_popcount(v); +#else + v = v - ((v >> 1) & 0x55555555u); + v = (v & 0x33333333u) + ((v >> 2) & 0x33333333u); + return static_cast((((v + (v >> 4)) & 0x0F0F0F0Fu) * 0x01010101u) >> 24); +#endif +} +#endif + +// True if every byte has the high bit clear (UTF-8 ASCII). +ADA_IDNA_REALLY_INLINE bool is_ascii_8(const char* data, size_t len) noexcept { + if (len == 0) { + return true; + } + const uint8_t* p = reinterpret_cast(data); +#if defined(ADA_IDNA_SSE2) + if (len >= 16) { + __m128i acc = _mm_setzero_si128(); + size_t i = 0; + for (; i + 16 <= len; i += 16) { + acc = _mm_or_si128( + acc, _mm_loadu_si128(reinterpret_cast(p + i))); + } + if (i < len) { + acc = _mm_or_si128( + acc, _mm_loadu_si128(reinterpret_cast(p + len - 16))); + } + return _mm_movemask_epi8(acc) == 0; + } +#elif defined(ADA_IDNA_NEON) + if (len >= 16) { + uint8x16_t acc = vdupq_n_u8(0); + size_t i = 0; + for (; i + 16 <= len; i += 16) { + acc = vorrq_u8(acc, vld1q_u8(p + i)); + } + if (i < len) { + acc = vorrq_u8(acc, vld1q_u8(p + len - 16)); + } + return vmaxvq_u8(acc) < 0x80; + } +#endif + uint64_t acc = 0; + size_t i = 0; + for (; i + 8 <= len; i += 8) { + uint64_t word = 0; + std::memcpy(&word, p + i, 8); + acc |= word; + } + if (i < len) { + uint64_t word = 0; + std::memcpy(&word, p + i, len - i); + acc |= word; + } + return (acc & 0x8080808080808080ull) == 0; +} + +// True if every UTF-32 code point is < 0x80. +ADA_IDNA_REALLY_INLINE bool is_ascii_32(const char32_t* data, + size_t len) noexcept { + if (len == 0) { + return true; + } + const uint32_t* p = reinterpret_cast(data); +#if defined(ADA_IDNA_SSE2) + if (len >= 4) { + const __m128i high = _mm_set1_epi32(static_cast(0xFFFFFF80u)); + __m128i acc = _mm_setzero_si128(); + size_t i = 0; + for (; i + 4 <= len; i += 4) { + acc = _mm_or_si128( + acc, _mm_loadu_si128(reinterpret_cast(p + i))); + } + if (i < len) { + acc = _mm_or_si128( + acc, _mm_loadu_si128(reinterpret_cast(p + len - 4))); + } + acc = _mm_and_si128(acc, high); + return _mm_movemask_epi8(_mm_cmpeq_epi32(acc, _mm_setzero_si128())) == + 0xFFFF; + } +#elif defined(ADA_IDNA_NEON) + if (len >= 4) { + uint32x4_t acc = vdupq_n_u32(0); + size_t i = 0; + for (; i + 4 <= len; i += 4) { + acc = vorrq_u32(acc, vld1q_u32(p + i)); + } + if (i < len) { + acc = vorrq_u32(acc, vld1q_u32(p + len - 4)); + } + return vmaxvq_u32(acc) < 0x80u; + } +#endif + uint64_t acc = 0; + size_t i = 0; + for (; i + 2 <= len; i += 2) { + uint64_t word = 0; + std::memcpy(&word, p + i, 8); + acc |= word; + } + if (i < len) { + acc |= p[i]; + } + return (acc & 0xFFFFFF80FFFFFF80ull) == 0; +} + +// In-place ASCII A-Z -> a-z. Caller guarantees bytes are ASCII (or accepts +// the same SWAR transform the scalar path already applied to any tail). +ADA_IDNA_REALLY_INLINE void ascii_lowercase(char* input, + size_t length) noexcept { + auto broadcast = [](uint8_t v) -> uint64_t { + return 0x101010101010101ull * v; + }; + const uint64_t broadcast_80 = broadcast(0x80); + const uint64_t broadcast_Ap = broadcast(static_cast(128 - 'A')); + const uint64_t broadcast_Zp = broadcast(static_cast(128 - 'Z' - 1)); + size_t i = 0; + +#if defined(ADA_IDNA_SSE2) + const __m128i v80 = _mm_set1_epi8(static_cast(0x80)); + const __m128i vAp = _mm_set1_epi8(static_cast(128 - 'A')); + const __m128i vZp = _mm_set1_epi8(static_cast(128 - 'Z' - 1)); + for (; i + 16 <= length; i += 16) { + __m128i word = _mm_loadu_si128(reinterpret_cast(input + i)); + __m128i mask = _mm_and_si128( + _mm_xor_si128(_mm_add_epi8(word, vAp), _mm_add_epi8(word, vZp)), v80); + // mask is 0x00 or 0x80 per byte; >>2 yields 0x00 or 0x20 without + // crossing byte lanes (low 2 bits of each byte are zero). + word = _mm_xor_si128(word, _mm_srli_epi16(mask, 2)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(input + i), word); + } +#elif defined(ADA_IDNA_NEON) + const uint8x16_t v80 = vdupq_n_u8(0x80); + const uint8x16_t vAp = vdupq_n_u8(static_cast(128 - 'A')); + const uint8x16_t vZp = vdupq_n_u8(static_cast(128 - 'Z' - 1)); + for (; i + 16 <= length; i += 16) { + uint8x16_t word = vld1q_u8(reinterpret_cast(input + i)); + uint8x16_t mask = + vandq_u8(veorq_u8(vaddq_u8(word, vAp), vaddq_u8(word, vZp)), v80); + word = veorq_u8(word, vshrq_n_u8(mask, 2)); + vst1q_u8(reinterpret_cast(input + i), word); + } +#endif + + for (; i + 8 <= length; i += 8) { + uint64_t word = 0; + std::memcpy(&word, input + i, sizeof(word)); + word ^= + (((word + broadcast_Ap) ^ (word + broadcast_Zp)) & broadcast_80) >> 2; + std::memcpy(input + i, &word, sizeof(word)); + } + if (i < length) { + uint64_t word = 0; + std::memcpy(&word, input + i, length - i); + word ^= + (((word + broadcast_Ap) ^ (word + broadcast_Zp)) & broadcast_80) >> 2; + std::memcpy(input + i, &word, length - i); + } +} + +// Widen 16 ASCII bytes to 16 UTF-32 code points. +ADA_IDNA_REALLY_INLINE void widen16_ascii_to_utf32(const uint8_t* src, + char32_t* dst) noexcept { +#if defined(ADA_IDNA_SSE2) + const __m128i bytes = _mm_loadu_si128(reinterpret_cast(src)); + const __m128i zero = _mm_setzero_si128(); + const __m128i lo16 = _mm_unpacklo_epi8(bytes, zero); + const __m128i hi16 = _mm_unpackhi_epi8(bytes, zero); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst), + _mm_unpacklo_epi16(lo16, zero)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 4), + _mm_unpackhi_epi16(lo16, zero)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 8), + _mm_unpacklo_epi16(hi16, zero)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 12), + _mm_unpackhi_epi16(hi16, zero)); +#elif defined(ADA_IDNA_NEON) + const uint8x16_t bytes = vld1q_u8(src); + const uint16x8_t lo16 = vmovl_u8(vget_low_u8(bytes)); + const uint16x8_t hi16 = vmovl_u8(vget_high_u8(bytes)); + vst1q_u32(reinterpret_cast(dst), vmovl_u16(vget_low_u16(lo16))); + vst1q_u32(reinterpret_cast(dst + 4), + vmovl_u16(vget_high_u16(lo16))); + vst1q_u32(reinterpret_cast(dst + 8), + vmovl_u16(vget_low_u16(hi16))); + vst1q_u32(reinterpret_cast(dst + 12), + vmovl_u16(vget_high_u16(hi16))); +#else + for (size_t k = 0; k < 16; ++k) { + dst[k] = static_cast(src[k]); + } +#endif +} + +// Pack 4 ASCII UTF-32 code points to 4 bytes. Caller guarantees values < 0x80. +ADA_IDNA_REALLY_INLINE void pack4_ascii_utf32(const uint32_t* src, + char* dst) noexcept { +#if defined(ADA_IDNA_SSE2) + __m128i v = _mm_loadu_si128(reinterpret_cast(src)); + v = _mm_and_si128(v, _mm_set1_epi32(0xFF)); + v = _mm_packs_epi32(v, v); + v = _mm_packus_epi16(v, v); + const uint32_t out = static_cast(_mm_cvtsi128_si32(v)); + std::memcpy(dst, &out, 4); +#elif defined(ADA_IDNA_NEON) + const uint32x4_t v = vld1q_u32(src); + const uint16x4_t n16 = vmovn_u32(v); + const uint8x8_t n8 = vmovn_u16(vcombine_u16(n16, n16)); + vst1_lane_u32(reinterpret_cast(dst), vreinterpret_u32_u8(n8), 0); +#else + dst[0] = static_cast(src[0]); + dst[1] = static_cast(src[1]); + dst[2] = static_cast(src[2]); + dst[3] = static_cast(src[3]); +#endif +} + +// True if 4 UTF-32 code points are all ASCII. +ADA_IDNA_REALLY_INLINE bool is_ascii_32x4(const uint32_t* src) noexcept { +#if defined(ADA_IDNA_SSE2) + const __m128i v = _mm_loadu_si128(reinterpret_cast(src)); + const __m128i masked = + _mm_and_si128(v, _mm_set1_epi32(static_cast(0xFFFFFF80u))); + return _mm_movemask_epi8(_mm_cmpeq_epi32(masked, _mm_setzero_si128())) == + 0xFFFF; +#elif defined(ADA_IDNA_NEON) + return vmaxvq_u32(vld1q_u32(src)) < 0x80u; +#else + uint64_t w0 = 0; + uint64_t w1 = 0; + std::memcpy(&w0, src, 8); + std::memcpy(&w1, src + 2, 8); + return ((w0 | w1) & 0xFFFFFF80FFFFFF80ull) == 0; +#endif +} + +// Count UTF-8 leading bytes (code points): bytes whose signed value is > -65. +ADA_IDNA_REALLY_INLINE size_t utf32_length_from_utf8(const char* buf, + size_t len) noexcept { + const int8_t* p = reinterpret_cast(buf); + size_t count = 0; + size_t i = 0; +#if defined(ADA_IDNA_SSE2) + const __m128i thresh = _mm_set1_epi8(static_cast(-65)); + for (; i + 16 <= len; i += 16) { + const __m128i v = _mm_loadu_si128(reinterpret_cast(p + i)); + const unsigned mask = + static_cast(_mm_movemask_epi8(_mm_cmpgt_epi8(v, thresh))); + count += static_cast(popcount_u32(mask)); + } +#elif defined(ADA_IDNA_NEON) + const int8x16_t thresh = vdupq_n_s8(static_cast(-65)); + for (; i + 16 <= len; i += 16) { + const int8x16_t v = vld1q_s8(p + i); + const uint8x16_t gt = vreinterpretq_u8_s8(vcgtq_s8(v, thresh)); + count += static_cast(vaddvq_u8(vandq_u8(gt, vdupq_n_u8(1)))); + } +#endif + for (; i < len; ++i) { + count += static_cast(p[i] > static_cast(-65)); + } + return count; +} + +// UTF-8 length of a UTF-32 sequence (1-4 bytes per code point). +ADA_IDNA_REALLY_INLINE size_t utf8_length_from_utf32(const char32_t* buf, + size_t len) noexcept { + const uint32_t* p = reinterpret_cast(buf); + size_t count = 0; + size_t i = 0; +#if defined(ADA_IDNA_SSE2) + const __m128i ones = _mm_set1_epi32(1); + const __m128i lim7f = _mm_set1_epi32(0x7F); + const __m128i lim7ff = _mm_set1_epi32(0x7FF); + const __m128i limffff = _mm_set1_epi32(0xFFFF); + for (; i + 4 <= len; i += 4) { + const __m128i v = _mm_loadu_si128(reinterpret_cast(p + i)); + // Signed compares are valid for Unicode code points (< 0x110000). + __m128i c = ones; + c = _mm_sub_epi32(c, _mm_cmpgt_epi32(v, lim7f)); + c = _mm_sub_epi32(c, _mm_cmpgt_epi32(v, lim7ff)); + c = _mm_sub_epi32(c, _mm_cmpgt_epi32(v, limffff)); + // Horizontal sum of 4 lanes. + __m128i shuf = _mm_shuffle_epi32(c, 0x4E); // swap 64-bit halves + c = _mm_add_epi32(c, shuf); + shuf = _mm_shuffle_epi32(c, 0xB1); // swap 32-bit pairs + c = _mm_add_epi32(c, shuf); + count += static_cast(static_cast(_mm_cvtsi128_si32(c))); + } +#elif defined(ADA_IDNA_NEON) + for (; i + 4 <= len; i += 4) { + const uint32x4_t v = vld1q_u32(p + i); + uint32x4_t c = vdupq_n_u32(1); + c = vaddq_u32(c, + vandq_u32(vcgtq_u32(v, vdupq_n_u32(0x7F)), vdupq_n_u32(1))); + c = vaddq_u32(c, + vandq_u32(vcgtq_u32(v, vdupq_n_u32(0x7FF)), vdupq_n_u32(1))); + c = vaddq_u32(c, + vandq_u32(vcgtq_u32(v, vdupq_n_u32(0xFFFF)), vdupq_n_u32(1))); + count += static_cast(vaddvq_u32(c)); + } +#endif + for (; i < len; ++i) { + ++count; + count += static_cast(p[i] > 0x7Fu); + count += static_cast(p[i] > 0x7FFu); + count += static_cast(p[i] > 0xFFFFu); + } + return count; +} + +// First index of `needle` in `data[0, len)`, or `len` if not found. +ADA_IDNA_REALLY_INLINE size_t find_char32(const char32_t* data, size_t len, + char32_t needle) noexcept { + const uint32_t* p = reinterpret_cast(data); + const uint32_t n = static_cast(needle); + size_t i = 0; +#if defined(ADA_IDNA_SSE2) + const __m128i nd = _mm_set1_epi32(static_cast(n)); + for (; i + 4 <= len; i += 4) { + const __m128i v = _mm_loadu_si128(reinterpret_cast(p + i)); + const int mask = _mm_movemask_ps(_mm_castsi128_ps(_mm_cmpeq_epi32(v, nd))); + if (mask != 0) { +#if defined(_MSC_VER) + unsigned long idx = 0; + _BitScanForward(&idx, static_cast(mask)); + return i + static_cast(idx); +#else + return i + + static_cast(__builtin_ctz(static_cast(mask))); +#endif + } + } +#elif defined(ADA_IDNA_NEON) + const uint32x4_t nd = vdupq_n_u32(n); + for (; i + 4 <= len; i += 4) { + const uint32x4_t eq = vceqq_u32(vld1q_u32(p + i), nd); + const uint64x2_t pair = vreinterpretq_u64_u32(eq); + const uint64_t lo = vgetq_lane_u64(pair, 0); + const uint64_t hi = vgetq_lane_u64(pair, 1); + if (lo != 0) { + return i + (static_cast(lo) == 0 ? 1 : 0); + } + if (hi != 0) { + return i + 2 + (static_cast(hi) == 0 ? 1 : 0); + } + } +#endif + for (; i < len; ++i) { + if (p[i] == n) { + return i; + } + } + return len; +} + +} // namespace ada::idna::simd + +#endif // ADA_IDNA_SIMD_HPP diff --git a/src/to_ascii.cpp b/src/to_ascii.cpp index 4b7a3b3..c235c5e 100644 --- a/src/to_ascii.cpp +++ b/src/to_ascii.cpp @@ -1,15 +1,13 @@ #include "ada/idna/to_ascii.h" -#include #include -#include -#include #include "ada/idna/mapping.h" #include "ada/idna/normalization.h" #include "ada/idna/punycode.h" #include "ada/idna/unicode_transcoding.h" #include "ada/idna/validity.h" +#include "simd.hpp" #ifdef ADA_USE_SIMDUTF #include "simdutf.h" @@ -17,22 +15,12 @@ namespace ada::idna { -bool constexpr is_ascii(std::u32string_view view) { - for (uint32_t c : view) { - if (c >= 0x80) { - return false; - } - } - return true; +bool is_ascii(std::u32string_view view) noexcept { + return simd::is_ascii_32(view.data(), view.size()); } -bool constexpr is_ascii(std::string_view view) { - for (uint8_t c : view) { - if (c >= 0x80) { - return false; - } - } - return true; +bool is_ascii(std::string_view view) noexcept { + return simd::is_ascii_8(view.data(), view.size()); } constexpr static uint8_t is_forbidden_domain_code_point_table[] = { @@ -50,12 +38,81 @@ constexpr static uint8_t is_forbidden_domain_code_point_table[] = { static_assert(sizeof(is_forbidden_domain_code_point_table) == 256); -inline bool is_forbidden_domain_code_point(const char c) noexcept { - return is_forbidden_domain_code_point_table[uint8_t(c)]; -} - bool contains_forbidden_domain_code_point(std::string_view view) { - return std::ranges::any_of(view, is_forbidden_domain_code_point); + // One pass: SIMD rejects C0 / space / non-ASCII (signed byte < 0x21), then + // an unrolled table covers the remaining forbidden ASCII punctuation. + const char* data = view.data(); + const size_t n = view.size(); + size_t i = 0; + uint8_t bits = 0; +#if defined(ADA_IDNA_SSE2) + const __m128i lim = _mm_set1_epi8(0x21); + for (; i + 16 <= n; i += 16) { + const __m128i word = + _mm_loadu_si128(reinterpret_cast(data + i)); + if (_mm_movemask_epi8(_mm_cmplt_epi8(word, lim)) != 0) { + return true; + } + bits = static_cast( + bits | is_forbidden_domain_code_point_table[uint8_t(data[i])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 1])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 2])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 3])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 4])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 5])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 6])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 7])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 8])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 9])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 10])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 11])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 12])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 13])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 14])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 15])]); + } +#elif defined(ADA_IDNA_NEON) + for (; i + 16 <= n; i += 16) { + const uint8x16_t word = + vld1q_u8(reinterpret_cast(data + i)); + if (vmaxvq_u8(vcltq_u8(word, vdupq_n_u8(0x21))) != 0) { + return true; + } + bits = static_cast( + bits | is_forbidden_domain_code_point_table[uint8_t(data[i])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 1])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 2])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 3])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 4])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 5])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 6])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 7])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 8])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 9])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 10])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 11])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 12])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 13])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 14])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 15])]); + } +#endif + for (; i + 8 <= n; i += 8) { + bits = static_cast( + bits | is_forbidden_domain_code_point_table[uint8_t(data[i])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 1])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 2])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 3])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 4])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 5])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 6])] | + is_forbidden_domain_code_point_table[uint8_t(data[i + 7])]); + } + for (; i < n; ++i) { + bits = static_cast( + bits | is_forbidden_domain_code_point_table[uint8_t(data[i])]); + } + return bits != 0; } // Per the WHATWG URL "domain to ASCII" algorithm, when beStrict is false and @@ -73,8 +130,13 @@ static void append_ascii_label(std::string& out, std::u32string_view label) { const size_t old = out.size(); out.resize(old + label.size()); char* dest = out.data() + old; - for (char32_t c : label) { - *dest++ = static_cast(c); + const uint32_t* src = reinterpret_cast(label.data()); + size_t i = 0; + for (; i + 4 <= label.size(); i += 4) { + simd::pack4_ascii_utf32(src + i, dest + i); + } + for (; i < label.size(); ++i) { + dest[i] = static_cast(src[i]); } } @@ -143,9 +205,8 @@ static bool is_ace_prefix(std::u32string_view label) noexcept { while (p < end) { const char32_t* label_begin = p; - while (p < end && *p != U'.') { - ++p; - } + const size_t remaining = static_cast(end - p); + p += simd::find_char32(p, remaining, U'.'); const size_t label_size = static_cast(p - label_begin); std::u32string_view label_view(label_begin, label_size); const bool is_last_label = (p == end); @@ -156,11 +217,9 @@ static bool is_ace_prefix(std::u32string_view label) noexcept { if (label_size == 0) { // empty label } else if (is_ace_prefix(label_view)) { - for (char32_t c : label_view) { - if (c >= 0x80) { - out.clear(); - return false; - } + if (!is_ascii(label_view)) { + out.clear(); + return false; } append_ascii_label(out, label_view); std::string_view puny_segment_ascii( diff --git a/src/unicode_transcoding.cpp b/src/unicode_transcoding.cpp index bc2708c..7f14ce4 100644 --- a/src/unicode_transcoding.cpp +++ b/src/unicode_transcoding.cpp @@ -1,9 +1,10 @@ #include "ada/idna/unicode_transcoding.h" -#include #include #include +#include "simd.hpp" + namespace ada::idna { size_t utf8_to_utf32(const char* buf, size_t len, char32_t* utf32_output) { @@ -11,7 +12,7 @@ size_t utf8_to_utf32(const char* buf, size_t len, char32_t* utf32_output) { size_t pos = 0; const char32_t* start{utf32_output}; while (pos < len) { - // try to convert the next block of 16 ASCII bytes + // Convert the next block of 16 ASCII bytes with a SIMD widen. if (pos + 16 <= len) { // if it is safe to read 16 more // bytes, check that they are ascii uint64_t v1; @@ -20,11 +21,9 @@ size_t utf8_to_utf32(const char* buf, size_t len, char32_t* utf32_output) { std::memcpy(&v2, data + pos + sizeof(uint64_t), sizeof(uint64_t)); uint64_t v{v1 | v2}; if ((v & 0x8080808080808080) == 0) { - size_t final_pos = pos + 16; - while (pos < final_pos) { - *utf32_output++ = char32_t(buf[pos]); - pos++; - } + simd::widen16_ascii_to_utf32(data + pos, utf32_output); + utf32_output += 16; + pos += 16; continue; } } @@ -105,24 +104,13 @@ size_t utf8_to_utf32(const char* buf, size_t len, char32_t* utf32_output) { size_t utf8_length_from_utf32(const char32_t* buf, size_t len) { // We are not BOM aware. - const uint32_t* p = reinterpret_cast(buf); - size_t counter{0}; - for (size_t i = 0; i != len; ++i) { - ++counter; // ASCII - counter += static_cast(p[i] > 0x7F); // two-byte - counter += static_cast(p[i] > 0x7FF); // three-byte - counter += static_cast(p[i] > 0xFFFF); // four-bytes - } - return counter; + return simd::utf8_length_from_utf32(buf, len); } size_t utf32_length_from_utf8(const char* buf, size_t len) { - const int8_t* p = reinterpret_cast(buf); - return std::count_if(p, std::next(p, len), [](int8_t c) { - // -65 is 0b10111111, anything larger in two-complement's - // should start a new code point. - return c > -65; - }); + // -65 is 0b10111111; anything larger in two's complement starts a + // new code point (not a UTF-8 continuation byte). + return simd::utf32_length_from_utf8(buf, len); } size_t utf32_to_utf8(const char32_t* buf, size_t len, char* utf8_output) { @@ -130,17 +118,12 @@ size_t utf32_to_utf8(const char32_t* buf, size_t len, char* utf8_output) { size_t pos = 0; const char* start{utf8_output}; while (pos < len) { - // try to convert the next block of 2 ASCII characters - if (pos + 2 <= len) { // if it is safe to read 8 more - // bytes, check that they are ascii - uint64_t v; - std::memcpy(&v, data + pos, sizeof(uint64_t)); - if ((v & 0xFFFFFF80FFFFFF80) == 0) { - *utf8_output++ = char(buf[pos]); - *utf8_output++ = char(buf[pos + 1]); - pos += 2; - continue; - } + // Convert the next block of 4 ASCII code points with a SIMD pack. + if (pos + 4 <= len && simd::is_ascii_32x4(data + pos)) { + simd::pack4_ascii_utf32(data + pos, utf8_output); + utf8_output += 4; + pos += 4; + continue; } uint32_t word = data[pos]; if ((word & 0xFFFFFF80) == 0) { diff --git a/tests/mapping_tests.cpp b/tests/mapping_tests.cpp index 6fc896f..c38840b 100644 --- a/tests/mapping_tests.cpp +++ b/tests/mapping_tests.cpp @@ -254,6 +254,30 @@ TEST(mapping_tests, string_multi_cp_mappings_in_context) { TEST(mapping_tests, empty_string) { EXPECT_EQ(ada::idna::map(U""), U""); } +TEST(mapping_tests, ascii_map_simd_widths) { + auto lower = [](std::string s) { + ada::idna::ascii_map(s.data(), s.size()); + return s; + }; + EXPECT_EQ(lower(""), ""); + EXPECT_EQ(lower("A"), "a"); + EXPECT_EQ(lower("Z"), "z"); + EXPECT_EQ(lower("a"), "a"); + EXPECT_EQ(lower("0123-._~"), "0123-._~"); + EXPECT_EQ(lower("ABCDEFg"), "abcdefg"); // 7 + EXPECT_EQ(lower("ABCDEFGH"), "abcdefgh"); // 8 + EXPECT_EQ(lower("ABCDEFGHIJKLMNO"), "abcdefghijklmno"); // 15 + EXPECT_EQ(lower("ABCDEFGHIJKLMNOP"), "abcdefghijklmnop"); // 16 + EXPECT_EQ(lower("ABCDEFGHIJKLMNOPQ"), "abcdefghijklmnopq"); // 17 + EXPECT_EQ(lower(std::string(32, 'A')), std::string(32, 'a')); + EXPECT_EQ(lower(std::string(33, 'Z')), std::string(33, 'z')); + EXPECT_EQ(lower("Example.COM"), "example.com"); + // Unaligned destination (offset 1 inside a larger buffer). + std::string padded = std::string(1, '!') + std::string(20, 'B'); + ada::idna::ascii_map(padded.data() + 1, 20); + EXPECT_EQ(padded, std::string(1, '!') + std::string(20, 'b')); +} + // ── CJK compatibility ideographs in the mapping range ───────────────────── // These are in the high area of the two-level table (0x2F800 range). TEST(mapping_tests, cjk_compatibility_ideographs) { diff --git a/tests/safety_tests.cpp b/tests/safety_tests.cpp index ec64751..ee19508 100644 --- a/tests/safety_tests.cpp +++ b/tests/safety_tests.cpp @@ -101,3 +101,120 @@ TEST(Safety, IsAlreadyNfc) { EXPECT_TRUE(ada::idna::is_already_nfc(precomposed)); EXPECT_FALSE(ada::idna::is_already_nfc(decomposed)); } + +TEST(Safety, IsAsciiUtf8LengthsAndAlignment) { + EXPECT_TRUE(ada::idna::is_ascii(std::string_view{})); + EXPECT_TRUE(ada::idna::is_ascii("")); + EXPECT_TRUE(ada::idna::is_ascii("a")); + EXPECT_TRUE(ada::idna::is_ascii("example.com")); + // 15 / 16 / 17 / 32 / 33 cover SWAR tail, SIMD block, and overlap. + const std::string a15(15, 'a'); + const std::string a16(16, 'b'); + const std::string a17(17, 'c'); + const std::string a32(32, 'd'); + const std::string a33(33, 'e'); + EXPECT_TRUE(ada::idna::is_ascii(a15)); + EXPECT_TRUE(ada::idna::is_ascii(a16)); + EXPECT_TRUE(ada::idna::is_ascii(a17)); + EXPECT_TRUE(ada::idna::is_ascii(a32)); + EXPECT_TRUE(ada::idna::is_ascii(a33)); + + std::string high_first = a16; + high_first[0] = static_cast(0x80); + EXPECT_FALSE(ada::idna::is_ascii(high_first)); + + std::string high_last16 = a16; + high_last16[15] = static_cast(0xFF); + EXPECT_FALSE(ada::idna::is_ascii(high_last16)); + + std::string high_mid32 = a32; + high_mid32[16] = static_cast(0xC3); + EXPECT_FALSE(ada::idna::is_ascii(high_mid32)); + + std::string high_tail = a33; + high_tail[32] = static_cast(0x80); + EXPECT_FALSE(ada::idna::is_ascii(high_tail)); + + // Unaligned view (offset 1) still uses unaligned SIMD loads. + std::string padded = std::string(1, 'z') + a32; + EXPECT_TRUE(ada::idna::is_ascii(std::string_view(padded).substr(1))); + padded[1 + 20] = static_cast(0x80); + EXPECT_FALSE(ada::idna::is_ascii(std::string_view(padded).substr(1))); +} + +TEST(Safety, IsAsciiUtf32Lengths) { + EXPECT_TRUE(ada::idna::is_ascii(std::u32string_view{})); + EXPECT_TRUE(ada::idna::is_ascii(std::u32string_view(U"abc"))); + EXPECT_TRUE(ada::idna::is_ascii(std::u32string(3, U'x'))); + EXPECT_TRUE(ada::idna::is_ascii(std::u32string(4, U'x'))); + EXPECT_TRUE(ada::idna::is_ascii(std::u32string(5, U'x'))); + EXPECT_TRUE(ada::idna::is_ascii(std::u32string(8, U'x'))); + EXPECT_TRUE(ada::idna::is_ascii(std::u32string(9, U'x'))); + + std::u32string mixed(8, U'a'); + mixed[0] = 0xE9; + EXPECT_FALSE(ada::idna::is_ascii(std::u32string_view(mixed))); + mixed[0] = U'a'; + mixed[3] = 0x80; + EXPECT_FALSE(ada::idna::is_ascii(std::u32string_view(mixed))); + mixed[3] = U'a'; + mixed[7] = 0x10FFFF; + EXPECT_FALSE(ada::idna::is_ascii(std::u32string_view(mixed))); +} + +TEST(Safety, ForbiddenDomainCodePointsSimdWidths) { + EXPECT_FALSE(ada::idna::contains_forbidden_domain_code_point("example.com")); + EXPECT_FALSE(ada::idna::contains_forbidden_domain_code_point("")); + const std::string ok16(16, 'a'); + const std::string ok32(32, 'b'); + EXPECT_FALSE(ada::idna::contains_forbidden_domain_code_point(ok16)); + EXPECT_FALSE(ada::idna::contains_forbidden_domain_code_point(ok32)); + + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point("foo bar")); + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point("foo#bar")); + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point("foo/bar")); + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point("foo:bar")); + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point("foo?bar")); + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point("foo@bar")); + + std::string high = ok16; + high[10] = static_cast(0x80); + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point(high)); + + std::string hash_at_16 = ok16 + "#tail"; + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point(hash_at_16)); +} + +TEST(Safety, UtfTranscodingAsciiBlocks) { + const std::string ascii32(32, 'A'); + const size_t n32 = + ada::idna::utf32_length_from_utf8(ascii32.data(), ascii32.size()); + EXPECT_EQ(n32, 32u); + std::u32string u32(n32, U'\0'); + EXPECT_EQ( + ada::idna::utf8_to_utf32(ascii32.data(), ascii32.size(), u32.data()), + 32u); + EXPECT_EQ(u32, std::u32string(32, U'A')); + EXPECT_EQ(ada::idna::utf8_length_from_utf32(u32.data(), u32.size()), 32u); + std::string back(32, '\0'); + EXPECT_EQ(ada::idna::utf32_to_utf8(u32.data(), u32.size(), back.data()), 32u); + EXPECT_EQ(back, ascii32); + + // Mixed: 16 ASCII + 2-byte UTF-8 + 16 ASCII (hits SIMD then scalar). + std::string mixed = std::string(16, 'x') + "\xc3\xa9" + std::string(16, 'y'); + const size_t n_mixed = + ada::idna::utf32_length_from_utf8(mixed.data(), mixed.size()); + EXPECT_EQ(n_mixed, 33u); + std::u32string u_mixed(n_mixed, U'\0'); + EXPECT_EQ( + ada::idna::utf8_to_utf32(mixed.data(), mixed.size(), u_mixed.data()), + 33u); + EXPECT_EQ(u_mixed[16], char32_t(0xE9)); + EXPECT_EQ(ada::idna::utf8_length_from_utf32(u_mixed.data(), u_mixed.size()), + mixed.size()); + std::string mixed_back(mixed.size(), '\0'); + EXPECT_EQ(ada::idna::utf32_to_utf8(u_mixed.data(), u_mixed.size(), + mixed_back.data()), + mixed.size()); + EXPECT_EQ(mixed_back, mixed); +} From 223864fb449a461c2727e3d54e779a6eaa9b7095 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 19:07:44 +0000 Subject: [PATCH 2/4] perf: fold ASCII check into lowercase and drop extra SIMD The common to_ascii path was two full scans (is_ascii, then ascii_map). Lowercase in place and test the high bit in one pass, matching ada-url's to_lower_ascii. Drop SIMD that increased instruction count on short domains: forbidden-code-point table stays a 4-way unroll, label-dot scan is scalar again, and UTF widen/pack share a single load. Process 32 bytes per lowercase/is_ascii iteration and accumulate utf8_length_from_utf32 instead of horizontally summing each block. Co-authored-by: Yagiz Nizipli --- src/mapping.cpp | 2 +- src/simd.hpp | 451 +++++++++++++++++------------------- src/to_ascii.cpp | 109 ++------- src/unicode_transcoding.cpp | 24 +- 4 files changed, 244 insertions(+), 342 deletions(-) diff --git a/src/mapping.cpp b/src/mapping.cpp index feae9e7..9028860 100644 --- a/src/mapping.cpp +++ b/src/mapping.cpp @@ -82,7 +82,7 @@ static size_t utf8_count_codepoints(const uint8_t* ptr) noexcept { // --- ASCII fast path --------------------------------------------------------- void ascii_map(char* input, size_t length) { - simd::ascii_lowercase(input, length); + (void)simd::ascii_lowercase_is_ascii(input, length); } // Two-pass map: first validate + exact size, then write once (no growth diff --git a/src/simd.hpp b/src/simd.hpp index 8a66765..e218690 100644 --- a/src/simd.hpp +++ b/src/simd.hpp @@ -1,9 +1,8 @@ #ifndef ADA_IDNA_SIMD_HPP #define ADA_IDNA_SIMD_HPP -// Portable SIMD helpers for IDNA hot paths (ASCII scan, lowercase, UTF -// widen/narrow). SSE2 is baseline on x86_64; NEON is baseline on aarch64. -// Other targets (s390x, RV64 without V, ...) use SWAR / scalar fallbacks. +// Portable SIMD helpers for IDNA hot paths. SSE2 is baseline on x86_64; +// NEON is baseline on aarch64. Other targets use SWAR. #include #include @@ -33,55 +32,171 @@ namespace ada::idna::simd { +// Byte-parallel ASCII A-Z -> a-z (Hacker's Delight / aqrit). +static constexpr uint64_t k80 = 0x8080808080808080ull; +static constexpr uint64_t kAp = 0x3F3F3F3F3F3F3F3Full; // 128 - 'A' +static constexpr uint64_t kZp = 0x2525252525252525ull; // 128 - 'Z' - 1 + +ADA_IDNA_REALLY_INLINE uint64_t lower8(uint64_t word) noexcept { + return word ^ ((((word + kAp) ^ (word + kZp)) & k80) >> 2); +} + #if defined(ADA_IDNA_SSE2) +ADA_IDNA_REALLY_INLINE __m128i lower16(__m128i word) noexcept { + const __m128i v80 = _mm_set1_epi8(static_cast(0x80)); + const __m128i vAp = _mm_set1_epi8(static_cast(128 - 'A')); + const __m128i vZp = _mm_set1_epi8(static_cast(128 - 'Z' - 1)); + const __m128i mask = _mm_and_si128( + _mm_xor_si128(_mm_add_epi8(word, vAp), _mm_add_epi8(word, vZp)), v80); + // mask is 0x00/0x80 per byte; >>2 -> 0x00/0x20 without crossing lanes. + return _mm_xor_si128(word, _mm_srli_epi16(mask, 2)); +} + +ADA_IDNA_REALLY_INLINE void widen16(__m128i bytes, char32_t* dst) noexcept { + const __m128i zero = _mm_setzero_si128(); + const __m128i lo16 = _mm_unpacklo_epi8(bytes, zero); + const __m128i hi16 = _mm_unpackhi_epi8(bytes, zero); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst), + _mm_unpacklo_epi16(lo16, zero)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 4), + _mm_unpackhi_epi16(lo16, zero)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 8), + _mm_unpacklo_epi16(hi16, zero)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 12), + _mm_unpackhi_epi16(hi16, zero)); +} + ADA_IDNA_REALLY_INLINE int popcount_u32(unsigned v) noexcept { -#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) +#if defined(_MSC_VER) return static_cast(__popcnt(v)); -#elif defined(__GNUC__) || defined(__clang__) - return __builtin_popcount(v); #else - v = v - ((v >> 1) & 0x55555555u); - v = (v & 0x33333333u) + ((v >> 2) & 0x33333333u); - return static_cast((((v + (v >> 4)) & 0x0F0F0F0Fu) * 0x01010101u) >> 24); + return __builtin_popcount(v); #endif } #endif -// True if every byte has the high bit clear (UTF-8 ASCII). -ADA_IDNA_REALLY_INLINE bool is_ascii_8(const char* data, size_t len) noexcept { - if (len == 0) { - return true; +#if defined(ADA_IDNA_NEON) +ADA_IDNA_REALLY_INLINE uint8x16_t lower16(uint8x16_t word) noexcept { + const uint8x16_t v80 = vdupq_n_u8(0x80); + const uint8x16_t vAp = vdupq_n_u8(static_cast(128 - 'A')); + const uint8x16_t vZp = vdupq_n_u8(static_cast(128 - 'Z' - 1)); + const uint8x16_t mask = + vandq_u8(veorq_u8(vaddq_u8(word, vAp), vaddq_u8(word, vZp)), v80); + return veorq_u8(word, vshrq_n_u8(mask, 2)); +} + +ADA_IDNA_REALLY_INLINE void widen16(uint8x16_t bytes, char32_t* dst) noexcept { + const uint16x8_t lo16 = vmovl_u8(vget_low_u8(bytes)); + const uint16x8_t hi16 = vmovl_u8(vget_high_u8(bytes)); + vst1q_u32(reinterpret_cast(dst), vmovl_u16(vget_low_u16(lo16))); + vst1q_u32(reinterpret_cast(dst + 4), + vmovl_u16(vget_high_u16(lo16))); + vst1q_u32(reinterpret_cast(dst + 8), + vmovl_u16(vget_low_u16(hi16))); + vst1q_u32(reinterpret_cast(dst + 12), + vmovl_u16(vget_high_u16(hi16))); +} +#endif + +// In-place A-Z -> a-z. Returns true iff every original byte was ASCII. +// One pass replaces a separate is_ascii scan plus ascii_map. +ADA_IDNA_REALLY_INLINE bool ascii_lowercase_is_ascii(char* input, + size_t length) noexcept { + bool ascii = true; + uint64_t high = 0; + size_t i = 0; +#if defined(ADA_IDNA_SSE2) + __m128i vacc = _mm_setzero_si128(); + for (; i + 32 <= length; i += 32) { + const __m128i w0 = + _mm_loadu_si128(reinterpret_cast(input + i)); + const __m128i w1 = + _mm_loadu_si128(reinterpret_cast(input + i + 16)); + vacc = _mm_or_si128(vacc, _mm_or_si128(w0, w1)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(input + i), lower16(w0)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(input + i + 16), lower16(w1)); + } + if (i + 16 <= length) { + const __m128i w = + _mm_loadu_si128(reinterpret_cast(input + i)); + vacc = _mm_or_si128(vacc, w); + _mm_storeu_si128(reinterpret_cast<__m128i*>(input + i), lower16(w)); + i += 16; } + ascii = _mm_movemask_epi8(vacc) == 0; +#elif defined(ADA_IDNA_NEON) + uint8x16_t vacc = vdupq_n_u8(0); + for (; i + 32 <= length; i += 32) { + const uint8x16_t w0 = vld1q_u8(reinterpret_cast(input + i)); + const uint8x16_t w1 = + vld1q_u8(reinterpret_cast(input + i + 16)); + vacc = vorrq_u8(vacc, vorrq_u8(w0, w1)); + vst1q_u8(reinterpret_cast(input + i), lower16(w0)); + vst1q_u8(reinterpret_cast(input + i + 16), lower16(w1)); + } + if (i + 16 <= length) { + const uint8x16_t w = vld1q_u8(reinterpret_cast(input + i)); + vacc = vorrq_u8(vacc, w); + vst1q_u8(reinterpret_cast(input + i), lower16(w)); + i += 16; + } + ascii = vmaxvq_u8(vacc) < 0x80; +#endif + for (; i + 8 <= length; i += 8) { + uint64_t word = 0; + std::memcpy(&word, input + i, 8); + high |= word; + word = lower8(word); + std::memcpy(input + i, &word, 8); + } + if (i < length) { + uint64_t word = 0; + std::memcpy(&word, input + i, length - i); + high |= word; + word = lower8(word); + std::memcpy(input + i, &word, length - i); + } + return ascii && (high & k80) == 0; +} + +ADA_IDNA_REALLY_INLINE bool is_ascii_8(const char* data, size_t len) noexcept { const uint8_t* p = reinterpret_cast(data); + uint64_t acc = 0; + size_t i = 0; #if defined(ADA_IDNA_SSE2) if (len >= 16) { - __m128i acc = _mm_setzero_si128(); - size_t i = 0; - for (; i + 16 <= len; i += 16) { - acc = _mm_or_si128( - acc, _mm_loadu_si128(reinterpret_cast(p + i))); + __m128i vacc = _mm_setzero_si128(); + for (; i + 32 <= len; i += 32) { + vacc = _mm_or_si128( + vacc, + _mm_or_si128( + _mm_loadu_si128(reinterpret_cast(p + i)), + _mm_loadu_si128(reinterpret_cast(p + i + 16)))); + } + if (i + 16 <= len) { + vacc = _mm_or_si128( + vacc, _mm_loadu_si128(reinterpret_cast(p + i))); + i += 16; } - if (i < len) { - acc = _mm_or_si128( - acc, _mm_loadu_si128(reinterpret_cast(p + len - 16))); + if (_mm_movemask_epi8(vacc) != 0) { + return false; } - return _mm_movemask_epi8(acc) == 0; } #elif defined(ADA_IDNA_NEON) if (len >= 16) { - uint8x16_t acc = vdupq_n_u8(0); - size_t i = 0; - for (; i + 16 <= len; i += 16) { - acc = vorrq_u8(acc, vld1q_u8(p + i)); + uint8x16_t vacc = vdupq_n_u8(0); + for (; i + 32 <= len; i += 32) { + vacc = vorrq_u8(vacc, vorrq_u8(vld1q_u8(p + i), vld1q_u8(p + i + 16))); + } + if (i + 16 <= len) { + vacc = vorrq_u8(vacc, vld1q_u8(p + i)); + i += 16; } - if (i < len) { - acc = vorrq_u8(acc, vld1q_u8(p + len - 16)); + if (vmaxvq_u8(vacc) >= 0x80) { + return false; } - return vmaxvq_u8(acc) < 0x80; } #endif - uint64_t acc = 0; - size_t i = 0; for (; i + 8 <= len; i += 8) { uint64_t word = 0; std::memcpy(&word, p + i, 8); @@ -92,46 +207,13 @@ ADA_IDNA_REALLY_INLINE bool is_ascii_8(const char* data, size_t len) noexcept { std::memcpy(&word, p + i, len - i); acc |= word; } - return (acc & 0x8080808080808080ull) == 0; + return (acc & k80) == 0; } -// True if every UTF-32 code point is < 0x80. +// SWAR over uint64 pairs: one load / two code points, no vector setup. ADA_IDNA_REALLY_INLINE bool is_ascii_32(const char32_t* data, size_t len) noexcept { - if (len == 0) { - return true; - } const uint32_t* p = reinterpret_cast(data); -#if defined(ADA_IDNA_SSE2) - if (len >= 4) { - const __m128i high = _mm_set1_epi32(static_cast(0xFFFFFF80u)); - __m128i acc = _mm_setzero_si128(); - size_t i = 0; - for (; i + 4 <= len; i += 4) { - acc = _mm_or_si128( - acc, _mm_loadu_si128(reinterpret_cast(p + i))); - } - if (i < len) { - acc = _mm_or_si128( - acc, _mm_loadu_si128(reinterpret_cast(p + len - 4))); - } - acc = _mm_and_si128(acc, high); - return _mm_movemask_epi8(_mm_cmpeq_epi32(acc, _mm_setzero_si128())) == - 0xFFFF; - } -#elif defined(ADA_IDNA_NEON) - if (len >= 4) { - uint32x4_t acc = vdupq_n_u32(0); - size_t i = 0; - for (; i + 4 <= len; i += 4) { - acc = vorrq_u32(acc, vld1q_u32(p + i)); - } - if (i < len) { - acc = vorrq_u32(acc, vld1q_u32(p + len - 4)); - } - return vmaxvq_u32(acc) < 0x80u; - } -#endif uint64_t acc = 0; size_t i = 0; for (; i + 2 <= len; i += 2) { @@ -145,137 +227,76 @@ ADA_IDNA_REALLY_INLINE bool is_ascii_32(const char32_t* data, return (acc & 0xFFFFFF80FFFFFF80ull) == 0; } -// In-place ASCII A-Z -> a-z. Caller guarantees bytes are ASCII (or accepts -// the same SWAR transform the scalar path already applied to any tail). -ADA_IDNA_REALLY_INLINE void ascii_lowercase(char* input, - size_t length) noexcept { - auto broadcast = [](uint8_t v) -> uint64_t { - return 0x101010101010101ull * v; - }; - const uint64_t broadcast_80 = broadcast(0x80); - const uint64_t broadcast_Ap = broadcast(static_cast(128 - 'A')); - const uint64_t broadcast_Zp = broadcast(static_cast(128 - 'Z' - 1)); - size_t i = 0; - -#if defined(ADA_IDNA_SSE2) - const __m128i v80 = _mm_set1_epi8(static_cast(0x80)); - const __m128i vAp = _mm_set1_epi8(static_cast(128 - 'A')); - const __m128i vZp = _mm_set1_epi8(static_cast(128 - 'Z' - 1)); - for (; i + 16 <= length; i += 16) { - __m128i word = _mm_loadu_si128(reinterpret_cast(input + i)); - __m128i mask = _mm_and_si128( - _mm_xor_si128(_mm_add_epi8(word, vAp), _mm_add_epi8(word, vZp)), v80); - // mask is 0x00 or 0x80 per byte; >>2 yields 0x00 or 0x20 without - // crossing byte lanes (low 2 bits of each byte are zero). - word = _mm_xor_si128(word, _mm_srli_epi16(mask, 2)); - _mm_storeu_si128(reinterpret_cast<__m128i*>(input + i), word); - } -#elif defined(ADA_IDNA_NEON) - const uint8x16_t v80 = vdupq_n_u8(0x80); - const uint8x16_t vAp = vdupq_n_u8(static_cast(128 - 'A')); - const uint8x16_t vZp = vdupq_n_u8(static_cast(128 - 'Z' - 1)); - for (; i + 16 <= length; i += 16) { - uint8x16_t word = vld1q_u8(reinterpret_cast(input + i)); - uint8x16_t mask = - vandq_u8(veorq_u8(vaddq_u8(word, vAp), vaddq_u8(word, vZp)), v80); - word = veorq_u8(word, vshrq_n_u8(mask, 2)); - vst1q_u8(reinterpret_cast(input + i), word); - } -#endif - - for (; i + 8 <= length; i += 8) { - uint64_t word = 0; - std::memcpy(&word, input + i, sizeof(word)); - word ^= - (((word + broadcast_Ap) ^ (word + broadcast_Zp)) & broadcast_80) >> 2; - std::memcpy(input + i, &word, sizeof(word)); - } - if (i < length) { - uint64_t word = 0; - std::memcpy(&word, input + i, length - i); - word ^= - (((word + broadcast_Ap) ^ (word + broadcast_Zp)) & broadcast_80) >> 2; - std::memcpy(input + i, &word, length - i); - } -} - -// Widen 16 ASCII bytes to 16 UTF-32 code points. -ADA_IDNA_REALLY_INLINE void widen16_ascii_to_utf32(const uint8_t* src, - char32_t* dst) noexcept { +// Load 16 bytes once: if ASCII, widen from the same register. +ADA_IDNA_REALLY_INLINE bool try_widen16_ascii(const uint8_t* src, + char32_t* dst) noexcept { #if defined(ADA_IDNA_SSE2) const __m128i bytes = _mm_loadu_si128(reinterpret_cast(src)); - const __m128i zero = _mm_setzero_si128(); - const __m128i lo16 = _mm_unpacklo_epi8(bytes, zero); - const __m128i hi16 = _mm_unpackhi_epi8(bytes, zero); - _mm_storeu_si128(reinterpret_cast<__m128i*>(dst), - _mm_unpacklo_epi16(lo16, zero)); - _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 4), - _mm_unpackhi_epi16(lo16, zero)); - _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 8), - _mm_unpacklo_epi16(hi16, zero)); - _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 12), - _mm_unpackhi_epi16(hi16, zero)); + if (_mm_movemask_epi8(bytes) != 0) { + return false; + } + widen16(bytes, dst); + return true; #elif defined(ADA_IDNA_NEON) const uint8x16_t bytes = vld1q_u8(src); - const uint16x8_t lo16 = vmovl_u8(vget_low_u8(bytes)); - const uint16x8_t hi16 = vmovl_u8(vget_high_u8(bytes)); - vst1q_u32(reinterpret_cast(dst), vmovl_u16(vget_low_u16(lo16))); - vst1q_u32(reinterpret_cast(dst + 4), - vmovl_u16(vget_high_u16(lo16))); - vst1q_u32(reinterpret_cast(dst + 8), - vmovl_u16(vget_low_u16(hi16))); - vst1q_u32(reinterpret_cast(dst + 12), - vmovl_u16(vget_high_u16(hi16))); + if (vmaxvq_u8(bytes) >= 0x80) { + return false; + } + widen16(bytes, dst); + return true; #else + uint64_t v1 = 0; + uint64_t v2 = 0; + std::memcpy(&v1, src, 8); + std::memcpy(&v2, src + 8, 8); + if (((v1 | v2) & k80) != 0) { + return false; + } for (size_t k = 0; k < 16; ++k) { dst[k] = static_cast(src[k]); } + return true; #endif } -// Pack 4 ASCII UTF-32 code points to 4 bytes. Caller guarantees values < 0x80. -ADA_IDNA_REALLY_INLINE void pack4_ascii_utf32(const uint32_t* src, - char* dst) noexcept { +// Load 4 UTF-32 values once: if ASCII, pack to 4 bytes. +ADA_IDNA_REALLY_INLINE bool try_pack4_ascii(const uint32_t* src, + char* dst) noexcept { #if defined(ADA_IDNA_SSE2) __m128i v = _mm_loadu_si128(reinterpret_cast(src)); - v = _mm_and_si128(v, _mm_set1_epi32(0xFF)); + if (_mm_movemask_ps( + _mm_castsi128_ps(_mm_cmpgt_epi32(v, _mm_set1_epi32(0x7F)))) != 0) { + return false; + } v = _mm_packs_epi32(v, v); v = _mm_packus_epi16(v, v); const uint32_t out = static_cast(_mm_cvtsi128_si32(v)); std::memcpy(dst, &out, 4); + return true; #elif defined(ADA_IDNA_NEON) const uint32x4_t v = vld1q_u32(src); - const uint16x4_t n16 = vmovn_u32(v); - const uint8x8_t n8 = vmovn_u16(vcombine_u16(n16, n16)); + if (vmaxvq_u32(v) >= 0x80u) { + return false; + } + const uint8x8_t n8 = vmovn_u16(vcombine_u16(vmovn_u32(v), vmovn_u32(v))); vst1_lane_u32(reinterpret_cast(dst), vreinterpret_u32_u8(n8), 0); -#else - dst[0] = static_cast(src[0]); - dst[1] = static_cast(src[1]); - dst[2] = static_cast(src[2]); - dst[3] = static_cast(src[3]); -#endif -} - -// True if 4 UTF-32 code points are all ASCII. -ADA_IDNA_REALLY_INLINE bool is_ascii_32x4(const uint32_t* src) noexcept { -#if defined(ADA_IDNA_SSE2) - const __m128i v = _mm_loadu_si128(reinterpret_cast(src)); - const __m128i masked = - _mm_and_si128(v, _mm_set1_epi32(static_cast(0xFFFFFF80u))); - return _mm_movemask_epi8(_mm_cmpeq_epi32(masked, _mm_setzero_si128())) == - 0xFFFF; -#elif defined(ADA_IDNA_NEON) - return vmaxvq_u32(vld1q_u32(src)) < 0x80u; + return true; #else uint64_t w0 = 0; uint64_t w1 = 0; std::memcpy(&w0, src, 8); std::memcpy(&w1, src + 2, 8); - return ((w0 | w1) & 0xFFFFFF80FFFFFF80ull) == 0; + if (((w0 | w1) & 0xFFFFFF80FFFFFF80ull) != 0) { + return false; + } + dst[0] = static_cast(src[0]); + dst[1] = static_cast(src[1]); + dst[2] = static_cast(src[2]); + dst[3] = static_cast(src[3]); + return true; #endif } -// Count UTF-8 leading bytes (code points): bytes whose signed value is > -65. ADA_IDNA_REALLY_INLINE size_t utf32_length_from_utf8(const char* buf, size_t len) noexcept { const int8_t* p = reinterpret_cast(buf); @@ -283,18 +304,26 @@ ADA_IDNA_REALLY_INLINE size_t utf32_length_from_utf8(const char* buf, size_t i = 0; #if defined(ADA_IDNA_SSE2) const __m128i thresh = _mm_set1_epi8(static_cast(-65)); + for (; i + 32 <= len; i += 32) { + const unsigned m0 = static_cast(_mm_movemask_epi8(_mm_cmpgt_epi8( + _mm_loadu_si128(reinterpret_cast(p + i)), thresh))); + const unsigned m1 = static_cast(_mm_movemask_epi8(_mm_cmpgt_epi8( + _mm_loadu_si128(reinterpret_cast(p + i + 16)), + thresh))); + count += static_cast(popcount_u32(m0) + popcount_u32(m1)); + } for (; i + 16 <= len; i += 16) { - const __m128i v = _mm_loadu_si128(reinterpret_cast(p + i)); const unsigned mask = - static_cast(_mm_movemask_epi8(_mm_cmpgt_epi8(v, thresh))); + static_cast(_mm_movemask_epi8(_mm_cmpgt_epi8( + _mm_loadu_si128(reinterpret_cast(p + i)), thresh))); count += static_cast(popcount_u32(mask)); } #elif defined(ADA_IDNA_NEON) const int8x16_t thresh = vdupq_n_s8(static_cast(-65)); + const uint8x16_t one = vdupq_n_u8(1); for (; i + 16 <= len; i += 16) { - const int8x16_t v = vld1q_s8(p + i); - const uint8x16_t gt = vreinterpretq_u8_s8(vcgtq_s8(v, thresh)); - count += static_cast(vaddvq_u8(vandq_u8(gt, vdupq_n_u8(1)))); + count += static_cast(vaddvq_u8( + vandq_u8(vreinterpretq_u8_s8(vcgtq_s8(vld1q_s8(p + i), thresh)), one))); } #endif for (; i < len; ++i) { @@ -303,7 +332,6 @@ ADA_IDNA_REALLY_INLINE size_t utf32_length_from_utf8(const char* buf, return count; } -// UTF-8 length of a UTF-32 sequence (1-4 bytes per code point). ADA_IDNA_REALLY_INLINE size_t utf8_length_from_utf32(const char32_t* buf, size_t len) noexcept { const uint32_t* p = reinterpret_cast(buf); @@ -314,32 +342,30 @@ ADA_IDNA_REALLY_INLINE size_t utf8_length_from_utf32(const char32_t* buf, const __m128i lim7f = _mm_set1_epi32(0x7F); const __m128i lim7ff = _mm_set1_epi32(0x7FF); const __m128i limffff = _mm_set1_epi32(0xFFFF); + __m128i acc = _mm_setzero_si128(); for (; i + 4 <= len; i += 4) { const __m128i v = _mm_loadu_si128(reinterpret_cast(p + i)); - // Signed compares are valid for Unicode code points (< 0x110000). __m128i c = ones; c = _mm_sub_epi32(c, _mm_cmpgt_epi32(v, lim7f)); c = _mm_sub_epi32(c, _mm_cmpgt_epi32(v, lim7ff)); c = _mm_sub_epi32(c, _mm_cmpgt_epi32(v, limffff)); - // Horizontal sum of 4 lanes. - __m128i shuf = _mm_shuffle_epi32(c, 0x4E); // swap 64-bit halves - c = _mm_add_epi32(c, shuf); - shuf = _mm_shuffle_epi32(c, 0xB1); // swap 32-bit pairs - c = _mm_add_epi32(c, shuf); - count += static_cast(static_cast(_mm_cvtsi128_si32(c))); + acc = _mm_add_epi32(acc, c); } + acc = _mm_add_epi32(acc, _mm_shuffle_epi32(acc, 0x4E)); + acc = _mm_add_epi32(acc, _mm_shuffle_epi32(acc, 0xB1)); + count = static_cast(static_cast(_mm_cvtsi128_si32(acc))); #elif defined(ADA_IDNA_NEON) + uint32x4_t acc = vdupq_n_u32(0); + const uint32x4_t one = vdupq_n_u32(1); for (; i + 4 <= len; i += 4) { const uint32x4_t v = vld1q_u32(p + i); - uint32x4_t c = vdupq_n_u32(1); - c = vaddq_u32(c, - vandq_u32(vcgtq_u32(v, vdupq_n_u32(0x7F)), vdupq_n_u32(1))); - c = vaddq_u32(c, - vandq_u32(vcgtq_u32(v, vdupq_n_u32(0x7FF)), vdupq_n_u32(1))); - c = vaddq_u32(c, - vandq_u32(vcgtq_u32(v, vdupq_n_u32(0xFFFF)), vdupq_n_u32(1))); - count += static_cast(vaddvq_u32(c)); + uint32x4_t c = one; + c = vaddq_u32(c, vandq_u32(vcgtq_u32(v, vdupq_n_u32(0x7F)), one)); + c = vaddq_u32(c, vandq_u32(vcgtq_u32(v, vdupq_n_u32(0x7FF)), one)); + c = vaddq_u32(c, vandq_u32(vcgtq_u32(v, vdupq_n_u32(0xFFFF)), one)); + acc = vaddq_u32(acc, c); } + count = static_cast(vaddvq_u32(acc)); #endif for (; i < len; ++i) { ++count; @@ -350,51 +376,6 @@ ADA_IDNA_REALLY_INLINE size_t utf8_length_from_utf32(const char32_t* buf, return count; } -// First index of `needle` in `data[0, len)`, or `len` if not found. -ADA_IDNA_REALLY_INLINE size_t find_char32(const char32_t* data, size_t len, - char32_t needle) noexcept { - const uint32_t* p = reinterpret_cast(data); - const uint32_t n = static_cast(needle); - size_t i = 0; -#if defined(ADA_IDNA_SSE2) - const __m128i nd = _mm_set1_epi32(static_cast(n)); - for (; i + 4 <= len; i += 4) { - const __m128i v = _mm_loadu_si128(reinterpret_cast(p + i)); - const int mask = _mm_movemask_ps(_mm_castsi128_ps(_mm_cmpeq_epi32(v, nd))); - if (mask != 0) { -#if defined(_MSC_VER) - unsigned long idx = 0; - _BitScanForward(&idx, static_cast(mask)); - return i + static_cast(idx); -#else - return i + - static_cast(__builtin_ctz(static_cast(mask))); -#endif - } - } -#elif defined(ADA_IDNA_NEON) - const uint32x4_t nd = vdupq_n_u32(n); - for (; i + 4 <= len; i += 4) { - const uint32x4_t eq = vceqq_u32(vld1q_u32(p + i), nd); - const uint64x2_t pair = vreinterpretq_u64_u32(eq); - const uint64_t lo = vgetq_lane_u64(pair, 0); - const uint64_t hi = vgetq_lane_u64(pair, 1); - if (lo != 0) { - return i + (static_cast(lo) == 0 ? 1 : 0); - } - if (hi != 0) { - return i + 2 + (static_cast(hi) == 0 ? 1 : 0); - } - } -#endif - for (; i < len; ++i) { - if (p[i] == n) { - return i; - } - } - return len; -} - } // namespace ada::idna::simd #endif // ADA_IDNA_SIMD_HPP diff --git a/src/to_ascii.cpp b/src/to_ascii.cpp index c235c5e..411d014 100644 --- a/src/to_ascii.cpp +++ b/src/to_ascii.cpp @@ -39,103 +39,31 @@ constexpr static uint8_t is_forbidden_domain_code_point_table[] = { static_assert(sizeof(is_forbidden_domain_code_point_table) == 256); bool contains_forbidden_domain_code_point(std::string_view view) { - // One pass: SIMD rejects C0 / space / non-ASCII (signed byte < 0x21), then - // an unrolled table covers the remaining forbidden ASCII punctuation. - const char* data = view.data(); + const auto* p = reinterpret_cast(view.data()); const size_t n = view.size(); size_t i = 0; uint8_t bits = 0; -#if defined(ADA_IDNA_SSE2) - const __m128i lim = _mm_set1_epi8(0x21); - for (; i + 16 <= n; i += 16) { - const __m128i word = - _mm_loadu_si128(reinterpret_cast(data + i)); - if (_mm_movemask_epi8(_mm_cmplt_epi8(word, lim)) != 0) { - return true; - } - bits = static_cast( - bits | is_forbidden_domain_code_point_table[uint8_t(data[i])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 1])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 2])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 3])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 4])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 5])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 6])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 7])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 8])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 9])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 10])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 11])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 12])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 13])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 14])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 15])]); - } -#elif defined(ADA_IDNA_NEON) - for (; i + 16 <= n; i += 16) { - const uint8x16_t word = - vld1q_u8(reinterpret_cast(data + i)); - if (vmaxvq_u8(vcltq_u8(word, vdupq_n_u8(0x21))) != 0) { - return true; - } - bits = static_cast( - bits | is_forbidden_domain_code_point_table[uint8_t(data[i])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 1])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 2])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 3])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 4])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 5])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 6])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 7])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 8])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 9])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 10])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 11])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 12])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 13])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 14])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 15])]); - } -#endif - for (; i + 8 <= n; i += 8) { - bits = static_cast( - bits | is_forbidden_domain_code_point_table[uint8_t(data[i])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 1])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 2])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 3])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 4])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 5])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 6])] | - is_forbidden_domain_code_point_table[uint8_t(data[i + 7])]); + for (; i + 4 <= n; i += 4) { + bits = + static_cast(bits | is_forbidden_domain_code_point_table[p[i]] | + is_forbidden_domain_code_point_table[p[i + 1]] | + is_forbidden_domain_code_point_table[p[i + 2]] | + is_forbidden_domain_code_point_table[p[i + 3]]); } for (; i < n; ++i) { - bits = static_cast( - bits | is_forbidden_domain_code_point_table[uint8_t(data[i])]); + bits = + static_cast(bits | is_forbidden_domain_code_point_table[p[i]]); } return bits != 0; } -// Per the WHATWG URL "domain to ASCII" algorithm, when beStrict is false and -// the input domain is an ASCII string, the result is the input lowercased, -// regardless of the outcome of Unicode ToASCII. -// -// See https://url.spec.whatwg.org/#concept-domain-to-ascii -static void from_ascii_to_ascii(std::string_view ut8_string, std::string& out) { - out.assign(ut8_string); - ascii_map(out.data(), out.size()); -} - // Append ASCII code units from a UTF-32 label (all values < 0x80). static void append_ascii_label(std::string& out, std::u32string_view label) { const size_t old = out.size(); out.resize(old + label.size()); char* dest = out.data() + old; - const uint32_t* src = reinterpret_cast(label.data()); - size_t i = 0; - for (; i + 4 <= label.size(); i += 4) { - simd::pack4_ascii_utf32(src + i, dest + i); - } - for (; i < label.size(); ++i) { + const char32_t* src = label.data(); + for (size_t i = 0; i < label.size(); ++i) { dest[i] = static_cast(src[i]); } } @@ -147,14 +75,18 @@ static bool is_ace_prefix(std::u32string_view label) noexcept { } [[nodiscard]] bool to_ascii(std::string_view ut8_string, std::string& out) { - out.clear(); if (ut8_string.size() > max_domain_input_bytes) { + out.clear(); return false; } - if (is_ascii(ut8_string)) { - from_ascii_to_ascii(ut8_string, out); + // WHATWG beStrict=false: ASCII input is just copied and lowercased. + // One pass: lowercase in place and test the high bit (no extra is_ascii + // scan). See https://url.spec.whatwg.org/#concept-domain-to-ascii + out.assign(ut8_string); + if (simd::ascii_lowercase_is_ascii(out.data(), out.size())) { return true; } + out.clear(); #ifdef ADA_USE_SIMDUTF size_t utf32_length = @@ -205,8 +137,9 @@ static bool is_ace_prefix(std::u32string_view label) noexcept { while (p < end) { const char32_t* label_begin = p; - const size_t remaining = static_cast(end - p); - p += simd::find_char32(p, remaining, U'.'); + while (p < end && *p != U'.') { + ++p; + } const size_t label_size = static_cast(p - label_begin); std::u32string_view label_view(label_begin, label_size); const bool is_last_label = (p == end); diff --git a/src/unicode_transcoding.cpp b/src/unicode_transcoding.cpp index 7f14ce4..ac8f0a9 100644 --- a/src/unicode_transcoding.cpp +++ b/src/unicode_transcoding.cpp @@ -1,7 +1,6 @@ #include "ada/idna/unicode_transcoding.h" #include -#include #include "simd.hpp" @@ -12,20 +11,11 @@ size_t utf8_to_utf32(const char* buf, size_t len, char32_t* utf32_output) { size_t pos = 0; const char32_t* start{utf32_output}; while (pos < len) { - // Convert the next block of 16 ASCII bytes with a SIMD widen. - if (pos + 16 <= len) { // if it is safe to read 16 more - // bytes, check that they are ascii - uint64_t v1; - std::memcpy(&v1, data + pos, sizeof(uint64_t)); - uint64_t v2; - std::memcpy(&v2, data + pos + sizeof(uint64_t), sizeof(uint64_t)); - uint64_t v{v1 | v2}; - if ((v & 0x8080808080808080) == 0) { - simd::widen16_ascii_to_utf32(data + pos, utf32_output); - utf32_output += 16; - pos += 16; - continue; - } + // One load: ASCII check and widen share the same 16-byte register. + if (pos + 16 <= len && simd::try_widen16_ascii(data + pos, utf32_output)) { + utf32_output += 16; + pos += 16; + continue; } uint8_t leading_byte = data[pos]; // leading byte if (leading_byte < 0b10000000) { @@ -118,9 +108,7 @@ size_t utf32_to_utf8(const char32_t* buf, size_t len, char* utf8_output) { size_t pos = 0; const char* start{utf8_output}; while (pos < len) { - // Convert the next block of 4 ASCII code points with a SIMD pack. - if (pos + 4 <= len && simd::is_ascii_32x4(data + pos)) { - simd::pack4_ascii_utf32(data + pos, utf8_output); + if (pos + 4 <= len && simd::try_pack4_ascii(data + pos, utf8_output)) { utf8_output += 4; pos += 4; continue; From a285ddae4d6631bf5d302ca840a7ead2568f1668 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 19:13:49 +0000 Subject: [PATCH 3/4] ci: pin Alpine to v3.22 so setup-alpine apk v2 can install packages latest-stable is now 3.23 (apk-tools v3). jirutka/setup-alpine still bootstraps the chroot with a static apk 2.14 binary, so apk add fails with "database: file format is invalid" on qemu ppc64le. Pin to v3.22 (last apk v2) and disable fail-fast so one arch does not cancel the rest. Co-authored-by: Yagiz Nizipli --- .github/workflows/alpine.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/alpine.yml b/.github/workflows/alpine.yml index 3fee540..1322631 100644 --- a/.github/workflows/alpine.yml +++ b/.github/workflows/alpine.yml @@ -8,6 +8,9 @@ jobs: name: Build on Alpine ${{ matrix.arch }} runs-on: ubuntu-latest strategy: + # setup-alpine + qemu can flake on one arch (apk DB under emulation); + # do not cancel the others. + fail-fast: false matrix: arch: - x86_64 @@ -22,11 +25,15 @@ jobs: - name: Install extra dependencies run: | sudo apt update && sudo apt install -y binfmt-support - - name: Install latest Alpine Linux for ${{ matrix.arch }} + - name: Install Alpine Linux for ${{ matrix.arch }} uses: jirutka/setup-alpine@v1 with: arch: ${{ matrix.arch }} - branch: ${{ matrix.arch == 'riscv64' && 'edge' || 'latest-stable' }} + # latest-stable is 3.23+ (apk-tools v3). setup-alpine still + # bootstraps with a static apk v2.14 binary, so apk add in the + # chroot fails with "database: file format is invalid" on some + # qemu arches (seen on ppc64le). v3.22 is the last apk v2 release. + branch: ${{ matrix.arch == 'riscv64' && 'edge' || 'v3.22' }} packages: > build-base cmake From 930719426c40073c16333465c125e28687c195ad Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 19:22:30 +0000 Subject: [PATCH 4/4] fix: use ACLE return type for NEON vcgtq_s8 Alpine aarch64 GCC 14 rejects vreinterpretq_u8_s8(vcgtq_s8(...)): vcgtq_s8 already returns uint8x16_t, so the reinterpret is a vector-type conversion error under -Wfatal-errors. Co-authored-by: Yagiz Nizipli --- src/simd.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/simd.hpp b/src/simd.hpp index e218690..d5afb5c 100644 --- a/src/simd.hpp +++ b/src/simd.hpp @@ -319,11 +319,12 @@ ADA_IDNA_REALLY_INLINE size_t utf32_length_from_utf8(const char* buf, count += static_cast(popcount_u32(mask)); } #elif defined(ADA_IDNA_NEON) + // vcgtq_s8 already returns uint8x16_t (ACLE). Do not vreinterpret. const int8x16_t thresh = vdupq_n_s8(static_cast(-65)); const uint8x16_t one = vdupq_n_u8(1); for (; i + 16 <= len; i += 16) { - count += static_cast(vaddvq_u8( - vandq_u8(vreinterpretq_u8_s8(vcgtq_s8(vld1q_s8(p + i), thresh)), one))); + const uint8x16_t gt = vcgtq_s8(vld1q_s8(p + i), thresh); + count += static_cast(vaddvq_u8(vandq_u8(gt, one))); } #endif for (; i < len; ++i) {