base16-2.0: cbits/base16_simd.c
/* Copyright (c) 2026, Emily Pillmore. BSD-3-Clause; see LICENSE. */
#include "base16_simd.h"
/*
* Original bounded prefix kernels. Target attributes keep x86 instructions
* out of baseline dispatch, with no global ISA flags or mutable state.
* HS_BASE16_SIMD_FORCE_SCALAR exercises the unsupported-target fallback.
* HS_BASE16_SIMD_FORCE_SSSE3 caps x86 dispatch for older-processor testing.
*/
#if !defined(HS_BASE16_SIMD_FORCE_SCALAR)
#if defined(__aarch64__) && defined(__ARM_NEON) && \
(defined(__GNUC__) || defined(__clang__))
#define HS_BASE16_NEON 1
#include <arm_neon.h>
#elif defined(__x86_64__) && (defined(__GNUC__) || defined(__clang__))
#define HS_BASE16_SSSE3 1
#include <immintrin.h>
#define HS_BASE16_TARGET_SSSE3 __attribute__((target("ssse3")))
#if !defined(HS_BASE16_SIMD_FORCE_SSSE3)
#define HS_BASE16_AVX2 1
#define HS_BASE16_TARGET_AVX2 __attribute__((target("avx2")))
#endif
#endif
#endif
#if defined(HS_BASE16_NEON) || defined(HS_BASE16_SSSE3)
static const uint8_t hs_base16_alphabet[] = "0123456789abcdef";
#endif
#if defined(HS_BASE16_NEON)
static size_t hs_base16_neon_encode(const uint8_t *src, size_t len, uint8_t *dst)
{
size_t consumed = 0;
const uint8x16_t alphabet = vld1q_u8(hs_base16_alphabet);
const uint8x16_t mask = vdupq_n_u8(15);
while (len - consumed >= 16) {
const uint8x16_t input = vld1q_u8(src + consumed);
uint8x16x2_t output;
output.val[0] = vqtbl1q_u8(alphabet, vshrq_n_u8(input, 4));
output.val[1] = vqtbl1q_u8(alphabet, vandq_u8(input, mask));
vst2q_u8(dst, output);
consumed += 16;
dst += 32;
}
return consumed;
}
static inline uint8x16_t hs_base16_neon_nibbles(uint8x16_t input,
uint8x16_t *valid)
{
const uint8x16_t digit = vsubq_u8(input, vdupq_n_u8('0'));
const uint8x16_t letter = vsubq_u8(vorrq_u8(input, vdupq_n_u8(0x20)),
vdupq_n_u8('a'));
const uint8x16_t is_digit = vcleq_u8(digit, vdupq_n_u8(9));
const uint8x16_t is_letter = vcleq_u8(letter, vdupq_n_u8(5));
*valid = vorrq_u8(is_digit, is_letter);
return vbslq_u8(is_letter, vaddq_u8(letter, vdupq_n_u8(10)), digit);
}
static size_t hs_base16_neon_decode(const uint8_t *src, size_t len, uint8_t *dst)
{
size_t consumed = 0;
while (len - consumed >= 32) {
const uint8x16x2_t input = vld2q_u8(src + consumed);
uint8x16_t valid_hi, valid_lo;
const uint8x16_t hi = hs_base16_neon_nibbles(input.val[0], &valid_hi);
const uint8x16_t lo = hs_base16_neon_nibbles(input.val[1], &valid_lo);
/* Validate the whole block before committing even its first byte. */
if (vminvq_u8(vandq_u8(valid_hi, valid_lo)) == 0) {
break;
}
vst1q_u8(dst, vorrq_u8(vshlq_n_u8(hi, 4), lo));
consumed += 32;
dst += 16;
}
return consumed;
}
#elif defined(HS_BASE16_SSSE3)
HS_BASE16_TARGET_SSSE3
static size_t hs_base16_ssse3_encode(const uint8_t *src, size_t len, uint8_t *dst)
{
size_t consumed = 0;
const __m128i alphabet = _mm_loadu_si128((const __m128i *)hs_base16_alphabet);
const __m128i mask = _mm_set1_epi8(15);
while (len - consumed >= 16) {
const __m128i input = _mm_loadu_si128((const __m128i *)(src + consumed));
const __m128i hi = _mm_shuffle_epi8(alphabet,
_mm_and_si128(_mm_srli_epi16(input, 4), mask));
const __m128i lo = _mm_shuffle_epi8(alphabet, _mm_and_si128(input, mask));
_mm_storeu_si128((__m128i *)dst, _mm_unpacklo_epi8(hi, lo));
_mm_storeu_si128((__m128i *)(dst + 16), _mm_unpackhi_epi8(hi, lo));
consumed += 16;
dst += 32;
}
return consumed;
}
HS_BASE16_TARGET_SSSE3
static inline __m128i hs_base16_ssse3_nibbles(__m128i input, __m128i *valid)
{
const __m128i lower = _mm_or_si128(input, _mm_set1_epi8(0x20));
const __m128i is_digit = _mm_and_si128(
_mm_cmpgt_epi8(input, _mm_set1_epi8('0' - 1)),
_mm_cmpgt_epi8(_mm_set1_epi8('9' + 1), input));
const __m128i is_letter = _mm_and_si128(
_mm_cmpgt_epi8(lower, _mm_set1_epi8('a' - 1)),
_mm_cmpgt_epi8(_mm_set1_epi8('f' + 1), lower));
/* Bytes >= 128 fail the signed lower-bound comparisons in both ranges. */
*valid = _mm_or_si128(is_digit, is_letter);
return _mm_add_epi8(_mm_and_si128(input, _mm_set1_epi8(15)),
_mm_and_si128(is_letter, _mm_set1_epi8(9)));
}
HS_BASE16_TARGET_SSSE3
static size_t hs_base16_ssse3_decode(const uint8_t *src, size_t len, uint8_t *dst)
{
size_t consumed = 0;
while (len - consumed >= 16) {
const __m128i input = _mm_loadu_si128((const __m128i *)(src + consumed));
__m128i valid;
const __m128i value = hs_base16_ssse3_nibbles(input, &valid);
__m128i pairs;
__m128i output;
if (_mm_movemask_epi8(valid) != 0xffff) {
break;
}
/* Adjacent nibbles form unsigned 0..255 words, without saturation. */
pairs = _mm_maddubs_epi16(value, _mm_set1_epi16(0x0110));
output = _mm_packus_epi16(pairs, _mm_setzero_si128());
_mm_storel_epi64((__m128i *)dst, output);
consumed += 16;
dst += 8;
}
return consumed;
}
#if defined(HS_BASE16_AVX2)
HS_BASE16_TARGET_AVX2
static size_t hs_base16_avx2_encode(const uint8_t *src, size_t len, uint8_t *dst)
{
size_t consumed = 0;
const __m256i alphabet = _mm256_broadcastsi128_si256(
_mm_loadu_si128((const __m128i *)hs_base16_alphabet));
const __m256i mask = _mm256_set1_epi8(15);
while (len - consumed >= 32) {
const __m256i input = _mm256_loadu_si256((const __m256i *)(src + consumed));
const __m256i hi = _mm256_shuffle_epi8(alphabet,
_mm256_and_si256(_mm256_srli_epi16(input, 4), mask));
const __m256i lo = _mm256_shuffle_epi8(alphabet, _mm256_and_si256(input, mask));
const __m256i first = _mm256_unpacklo_epi8(hi, lo);
const __m256i second = _mm256_unpackhi_epi8(hi, lo);
/* Unpacks operate within 128-bit lanes; restore original byte order. */
_mm256_storeu_si256((__m256i *)dst,
_mm256_permute2x128_si256(first, second, 0x20));
_mm256_storeu_si256((__m256i *)(dst + 32),
_mm256_permute2x128_si256(first, second, 0x31));
consumed += 32;
dst += 64;
}
return consumed;
}
HS_BASE16_TARGET_AVX2
static size_t hs_base16_avx2_decode(const uint8_t *src, size_t len, uint8_t *dst)
{
size_t consumed = 0;
while (len - consumed >= 32) {
const __m256i input = _mm256_loadu_si256((const __m256i *)(src + consumed));
const __m256i lower = _mm256_or_si256(input, _mm256_set1_epi8(0x20));
const __m256i is_digit = _mm256_and_si256(
_mm256_cmpgt_epi8(input, _mm256_set1_epi8('0' - 1)),
_mm256_cmpgt_epi8(_mm256_set1_epi8('9' + 1), input));
const __m256i is_letter = _mm256_and_si256(
_mm256_cmpgt_epi8(lower, _mm256_set1_epi8('a' - 1)),
_mm256_cmpgt_epi8(_mm256_set1_epi8('f' + 1), lower));
__m256i value;
__m256i pairs;
__m128i output;
if (_mm256_movemask_epi8(_mm256_or_si256(is_digit, is_letter)) != -1) {
break;
}
value = _mm256_add_epi8(_mm256_and_si256(input, _mm256_set1_epi8(15)),
_mm256_and_si256(is_letter, _mm256_set1_epi8(9)));
pairs = _mm256_maddubs_epi16(value, _mm256_set1_epi16(0x0110));
/* Pack both lanes into exactly 16 output bytes, without lane gaps. */
output = _mm_packus_epi16(_mm256_castsi256_si128(pairs),
_mm256_extracti128_si256(pairs, 1));
_mm_storeu_si128((__m128i *)dst, output);
consumed += 32;
dst += 16;
}
return consumed;
}
#endif /* HS_BASE16_AVX2 */
#endif
int hs_base16_simd_backend(void)
{
#if defined(HS_BASE16_NEON)
return 2;
#elif defined(HS_BASE16_SSSE3)
#if defined(HS_BASE16_AVX2)
/* The compiler's query also checks OS support for AVX register state. */
if (__builtin_cpu_supports("avx2")) {
return 3;
}
#endif
return __builtin_cpu_supports("ssse3") ? 1 : 0;
#else
return 0;
#endif
}
size_t hs_base16_simd_encode(const uint8_t *src, size_t len, uint8_t *dst)
{
#if defined(HS_BASE16_NEON)
return hs_base16_neon_encode(src, len, dst);
#elif defined(HS_BASE16_SSSE3)
const int backend = hs_base16_simd_backend();
#if defined(HS_BASE16_AVX2)
if (backend == 3) {
return hs_base16_avx2_encode(src, len, dst);
}
#endif
if (backend == 1) {
return hs_base16_ssse3_encode(src, len, dst);
}
#endif
(void)src;
(void)len;
(void)dst;
return 0;
}
size_t hs_base16_simd_decode(const uint8_t *src, size_t len, uint8_t *dst)
{
#if defined(HS_BASE16_NEON)
return hs_base16_neon_decode(src, len, dst);
#elif defined(HS_BASE16_SSSE3)
const int backend = hs_base16_simd_backend();
#if defined(HS_BASE16_AVX2)
if (backend == 3) {
return hs_base16_avx2_decode(src, len, dst);
}
#endif
if (backend == 1) {
return hs_base16_ssse3_decode(src, len, dst);
}
#endif
(void)src;
(void)len;
(void)dst;
return 0;
}