Skip to content

Commit b4319b0

Browse files
committed
Improve: Pick SVE over NEON by measured register width
`SZ_ENFORCE_SVE_OVER_NEON` pretended to be a global policy switch but only half the kernel families honored it (compare, memory, find, UTF-8 tokens - not hash or intersect), its "enforce everywhere" README promise was false, and as a compile-time constant it answered a question that is inherently per-machine: whether the scalable kernels beat NEON depends on the SVE register width, which Arm implementations choose freely. Replace the flag with the width question asked directly, once per dispatch context: - Runtime dispatch measures the running CPU with `svcntb` through the new `sz_sve_wider_than_neon_` helper in `dispatch.h`, next to the dispatch table it serves. At the common 128-bit vector length the length-sensitive families keep their faster NEON kernels; wider implementations get the scalable ones automatically. - Compile-time dispatch consults `SZ_SVE_WIDER_THAN_NEON_` in `types.h`, driven by `__ARM_FEATURE_SVE_BITS` when the width is pinned via `-msve-vector-bits=N`; unpinned builds assume the common 128-bit case and keep NEON, matching the flag's old default behavior bit for bit. The crypto-heavy families are untouched and prefer SVE2 at any width. On 128-bit implementations like Graviton 4 the four families now stay on NEON, matching measurements; on wider ones like Graviton 3 (256-bit) or A64FX (512-bit) the scalable kernels are finally selected automatically - a combination the old default could never reach. To force a specific backend regardless, the `SZ_USE_SVE`/`SZ_USE_SVE2`/`SZ_USE_NEON` toggles remain. Verified with all Arm tiers enabled across all sixteen C translation units under Clang 20 at a plain `armv8-a` baseline, GCC 12 on Linux-Arm with `-Werror=implicit-function-declaration`, and both Cargo dispatch modes.
1 parent 124d7e9 commit b4319b0

11 files changed

Lines changed: 57 additions & 23 deletions

File tree

c/stringzilla/compare.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ SZ_DISPATCH_INTERNAL void sz_dispatch_compare_update_(sz_capability_t caps) {
4141

4242
#if SZ_USE_SVE
4343
if (caps & sz_cap_sve_k) {
44-
if (SZ_ENFORCE_SVE_OVER_NEON) {
44+
// Wider-than-NEON registers are where the scalable comparison kernels win; at the common
45+
// 128-bit vector length the NEON kernels stay faster, so keep them.
46+
if (sz_sve_wider_than_neon_()) {
4547
impl->equal = sz_equal_sve;
4648
impl->order = sz_order_sve;
4749
}

c/stringzilla/dispatch.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,24 @@ SZ_DISPATCH_INTERNAL void sz_dispatch_utf8_uncased_fold_update_(sz_capability_t
108108
SZ_DISPATCH_INTERNAL void sz_dispatch_utf8_norm_update_(sz_capability_t caps);
109109
SZ_DISPATCH_INTERNAL void sz_dispatch_utf8_uncased_update_(sz_capability_t caps);
110110

111+
#if SZ_IS_64BIT_ARM_ && (SZ_USE_SVE || SZ_USE_SVE2) && !defined(_MSC_VER)
112+
#if defined(__clang__)
113+
#pragma clang attribute push(__attribute__((target("+sve"))), apply_to = function)
114+
#elif defined(__GNUC__)
115+
#pragma GCC push_options
116+
#pragma GCC target("+sve")
117+
#endif
118+
/**
119+
* @brief Whether the running CPU's SVE registers are wider than NEON's 128 bits - the point where the
120+
* scalable kernels start outrunning NEON in the length-sensitive families (the runtime
121+
* counterpart of the compile-time `SZ_SVE_WIDER_THAN_NEON_` in `types.h`).
122+
*/
123+
SZ_MAYBE_UNUSED SZ_C_INLINE sz_bool_t sz_sve_wider_than_neon_(void) { return svcntb() > 16 ? sz_true_k : sz_false_k; }
124+
#if defined(__clang__)
125+
#pragma clang attribute pop
126+
#elif defined(__GNUC__)
127+
#pragma GCC pop_options
128+
#endif
129+
#endif // SZ_IS_64BIT_ARM_ && (SZ_USE_SVE || SZ_USE_SVE2) && !defined(_MSC_VER)
130+
111131
#endif // STRINGZILLA_DISPATCH_H_

c/stringzilla/find.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ SZ_DISPATCH_INTERNAL void sz_dispatch_find_update_(sz_capability_t caps) {
7878

7979
#if SZ_USE_SVE
8080
if (caps & sz_cap_sve_k) {
81-
if (SZ_ENFORCE_SVE_OVER_NEON) {
81+
// Wider-than-NEON registers are where the scalable substring kernels win; at the common
82+
// 128-bit vector length the NEON kernels stay faster, so keep them.
83+
if (sz_sve_wider_than_neon_()) {
8284
impl->find = sz_find_sve;
8385
// TODO: impl->rfind = sz_rfind_sve;
8486
}

c/stringzilla/memory.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ SZ_DISPATCH_INTERNAL void sz_dispatch_memory_update_(sz_capability_t caps) {
5959

6060
#if SZ_USE_SVE
6161
if (caps & sz_cap_sve_k) {
62-
if (SZ_ENFORCE_SVE_OVER_NEON) {
62+
// Wider-than-NEON registers are where the scalable memory kernels win; at the common
63+
// 128-bit vector length the NEON kernels stay faster, so keep them.
64+
if (sz_sve_wider_than_neon_()) {
6365
impl->copy = sz_copy_sve;
6466
impl->move = sz_move_sve;
6567
impl->fill = sz_fill_sve;

c/stringzilla/utf8_tokens.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ SZ_DISPATCH_INTERNAL void sz_dispatch_utf8_tokens_update_(sz_capability_t caps)
4040

4141
#if SZ_USE_SVE2
4242
if (caps & sz_cap_sve2_k) {
43-
if (SZ_ENFORCE_SVE_OVER_NEON) {
43+
// Wider-than-NEON registers are where the scalable token-scanning kernels win; at the common
44+
// 128-bit vector length the NEON kernels stay faster, so keep them.
45+
if (sz_sve_wider_than_neon_()) {
4446
impl->utf8_newlines = sz_utf8_newlines_sve2;
4547
impl->utf8_whitespaces = sz_utf8_whitespaces_sve2;
4648
}

include/stringzilla/README.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,10 @@ The runtime probe inspects CPUID on x86, the AArch64 ID registers on ARM with a
781781
Detection always reports the full hardware truth, independent of which tiers a build compiled in; `sz_capabilities()` intersects it with the compile-time mask.
782782
WebAssembly is the exception with no runtime probe at all — a module carrying unsupported SIMD opcodes fails validation at instantiation, so its capabilities are fixed at compile time (`SZ_CAPABILITIES_RUNTIME_DETECTABLE_` reports whether the current platform can introspect).
783783

784+
On Arm, having SVE in the capability mask doesn't mean SVE kernels always win: at the common 128-bit vector length the scalable kernels for length-sensitive operations — comparisons, memory transforms, substring search, UTF-8 token scanning — are often slower than their NEON twins, while crypto-heavy operations like hashing prefer SVE2 at any width.
785+
Dispatch therefore picks by register width: the load-time table measures it on the running CPU (`svcntb`), and compile-time dispatch consults `__ARM_FEATURE_SVE_BITS` when pinned via `-msve-vector-bits=N`, otherwise assuming the 128-bit case and keeping NEON.
786+
To force a specific backend regardless, use the `SZ_USE_SVE`/`SZ_USE_SVE2`/`SZ_USE_NEON` toggles.
787+
784788
The same machinery drives the build systems through the checked-in `probes/` programs: CMake and Cargo try-compile `probes/<arch>_<tier>.c` — tiny standalone programs reusing the real kernels' `target` pragmas, intrinsics, and platform guards — to learn which tiers the toolchain can __compile__, and execute `probes/run_capabilities.c` to learn which tiers the build machine can __run__.
785789
Runtime-dispatched libraries enable everything compilable, trusting the load-time table to mask the rest; compile-time builds bake the intersection of the two sets.
786790

@@ -1011,13 +1015,6 @@ __`SZ_USE_CUDA`, `SZ_USE_KEPLER`, `SZ_USE_HOPPER`__:
10111015
> One can explicitly disable certain families of PTX instructions for compatibility purposes.
10121016
> Default values are inferred at compile time depending on compiler support (for dynamic dispatch) and the target architecture (for static dispatch).
10131017
1014-
__`SZ_ENFORCE_SVE_OVER_NEON`__:
1015-
1016-
> SVE and SVE2 are expected to supersede NEON on ARM architectures.
1017-
> Still, oftentimes the equivalent SVE kernels are slower due to equally small register files and higher complexity of the instructions.
1018-
> By default, when both SVE and NEON are available, SVE is used selectively only for the algorithms that benefit from it.
1019-
> If you want to enforce SVE usage everywhere, define this flag.
1020-
10211018
__`SZ_DYNAMIC_DISPATCH`__:
10221019
10231020
> By default, StringZilla is a header-only library.

include/stringzilla/compare.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ SZ_API_RUNTIME sz_bool_t sz_equal(sz_cptr_t a, sz_cptr_t b, sz_size_t length) {
197197
return sz_equal_skylake(a, b, length);
198198
#elif SZ_USE_HASWELL
199199
return sz_equal_haswell(a, b, length);
200-
#elif SZ_USE_SVE && SZ_ENFORCE_SVE_OVER_NEON
200+
#elif SZ_USE_SVE && SZ_SVE_WIDER_THAN_NEON_
201201
return sz_equal_sve(a, b, length);
202202
#elif SZ_USE_NEON
203203
return sz_equal_neon(a, b, length);
@@ -221,7 +221,7 @@ SZ_API_RUNTIME sz_ordering_t sz_order(sz_cptr_t a, sz_size_t a_length, sz_cptr_t
221221
return sz_order_skylake(a, a_length, b, b_length);
222222
#elif SZ_USE_HASWELL
223223
return sz_order_haswell(a, a_length, b, b_length);
224-
#elif SZ_USE_SVE && SZ_ENFORCE_SVE_OVER_NEON
224+
#elif SZ_USE_SVE && SZ_SVE_WIDER_THAN_NEON_
225225
return sz_order_sve(a, a_length, b, b_length);
226226
#elif SZ_USE_NEON
227227
return sz_order_neon(a, a_length, b, b_length);

include/stringzilla/find.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ SZ_API_RUNTIME sz_cptr_t sz_find(sz_cptr_t haystack, sz_size_t haystack_length,
513513
return sz_find_haswell(haystack, haystack_length, needle, needle_length);
514514
#elif SZ_USE_WESTMERE
515515
return sz_find_westmere(haystack, haystack_length, needle, needle_length);
516-
#elif SZ_USE_SVE && SZ_ENFORCE_SVE_OVER_NEON
516+
#elif SZ_USE_SVE && SZ_SVE_WIDER_THAN_NEON_
517517
return sz_find_sve(haystack, haystack_length, needle, needle_length);
518518
#elif SZ_USE_NEON
519519
return sz_find_neon(haystack, haystack_length, needle, needle_length);

include/stringzilla/memory.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ SZ_API_RUNTIME void sz_copy(sz_ptr_t target, sz_cptr_t source, sz_size_t length)
417417
sz_copy_skylake(target, source, length);
418418
#elif SZ_USE_HASWELL
419419
sz_copy_haswell(target, source, length);
420-
#elif SZ_USE_SVE && SZ_ENFORCE_SVE_OVER_NEON
420+
#elif SZ_USE_SVE && SZ_SVE_WIDER_THAN_NEON_
421421
sz_copy_sve(target, source, length);
422422
#elif SZ_USE_NEON
423423
sz_copy_neon(target, source, length);
@@ -441,7 +441,7 @@ SZ_API_RUNTIME void sz_move(sz_ptr_t target, sz_cptr_t source, sz_size_t length)
441441
sz_move_skylake(target, source, length);
442442
#elif SZ_USE_HASWELL
443443
sz_move_haswell(target, source, length);
444-
#elif SZ_USE_SVE && SZ_ENFORCE_SVE_OVER_NEON
444+
#elif SZ_USE_SVE && SZ_SVE_WIDER_THAN_NEON_
445445
sz_move_sve(target, source, length);
446446
#elif SZ_USE_NEON
447447
sz_move_neon(target, source, length);
@@ -465,7 +465,7 @@ SZ_API_RUNTIME void sz_fill(sz_ptr_t target, sz_size_t length, sz_u8_t value) {
465465
sz_fill_skylake(target, length, value);
466466
#elif SZ_USE_HASWELL
467467
sz_fill_haswell(target, length, value);
468-
#elif SZ_USE_SVE && SZ_ENFORCE_SVE_OVER_NEON
468+
#elif SZ_USE_SVE && SZ_SVE_WIDER_THAN_NEON_
469469
sz_fill_sve(target, length, value);
470470
#elif SZ_USE_NEON
471471
sz_fill_neon(target, length, value);
@@ -489,7 +489,7 @@ SZ_API_RUNTIME void sz_lookup(sz_ptr_t target, sz_size_t length, sz_cptr_t sourc
489489
sz_lookup_icelake(target, length, source, lut);
490490
#elif SZ_USE_HASWELL
491491
sz_lookup_haswell(target, length, source, lut);
492-
#elif SZ_USE_SVE && SZ_ENFORCE_SVE_OVER_NEON
492+
#elif SZ_USE_SVE && SZ_SVE_WIDER_THAN_NEON_
493493
sz_lookup_sve(target, length, source, lut);
494494
#elif SZ_USE_NEON
495495
sz_lookup_neon(target, length, source, lut);

include/stringzilla/types.h

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,9 +399,18 @@
399399
#endif
400400
#endif
401401

402-
/** SVE isn't a silver bullet. Oftentimes its still slower and with this flag you can disable it. */
403-
#if !defined(SZ_ENFORCE_SVE_OVER_NEON)
404-
#define SZ_ENFORCE_SVE_OVER_NEON (0)
402+
/**
403+
* SVE isn't a silver bullet: in the length-sensitive kernel families (compare, memory, find, UTF-8
404+
* tokens) the scalable kernels only outrun NEON when the registers are wider than NEON's 128 bits,
405+
* while the crypto-heavy families (like hashing) win on SVE2 at any width. At compile time the width
406+
* is only known when pinned via `-msve-vector-bits=N` (`__ARM_FEATURE_SVE_BITS`); unpinned builds
407+
* assume the common 128-bit case and keep NEON. Runtime dispatch measures the actual width with
408+
* `sz_sve_wider_than_neon_` instead of trusting this compile-time assumption.
409+
*/
410+
#if defined(__ARM_FEATURE_SVE_BITS) && (__ARM_FEATURE_SVE_BITS > 128)
411+
#define SZ_SVE_WIDER_THAN_NEON_ (1)
412+
#else
413+
#define SZ_SVE_WIDER_THAN_NEON_ (0)
405414
#endif
406415

407416
/**

0 commit comments

Comments
 (0)