Skip to content

Commit 3176c1f

Browse files
perf(wcscmp): portable-SIMD wide compare for wcscmp/wcsncmp (bd-2g7oyh.366)
wcscmp/wcsncmp compared wchar_t (u32) one element at a time. Add scan_wcscmp_simd: a fused 8-lane Simd<u32,8> compare — equal-and-NUL-free 32-byte windows advance 8 elements, others resolve element-wise (byte-identical to the scalar loop incl. shared-NUL=>0 and signed wchar_t ordering). Dual pointers can't be pre-aligned, so each 32-byte vector load is page-cross guarded (wide32_read_within_page, (addr & 0xFFF) <= 4064); 8 lanes per window amortise that guard cost — unlike a 2-lane u64-SWAR, which lost to scalar last session. Raw [u32;8] array loads (4-byte aligned, never a Rust slice over C memory), matching wcschr. wcsncmp keeps the nuanced adverse-only-on-membrane-clamp logic. Measured (rch, median ns/op, equal wide strings full scan): wchars old(ns) new(ns) glibc(ns) self vs-glibc 64 22.5 9.1 17.8 2.48x 1.96x 256 67.5 28.9 36.7 2.34x 1.27x 1024 332.6 131.4 176.2 2.53x 1.34x 4096 1042.5 542.0 463.8 1.92x 0.86x 65536 19377.4 9555.5 7843.3 2.03x 0.82x Self-speedup 1.66-2.53x (>=2.0 for n in {64,256,1024,16384,65536}); beats glibc for the common short/mid strings (<=1024 + 16384) and closes the gap at large sizes (old was 0.44x glibc at 4096; new 0.86x — the portable-SIMD vs hand-AVX2 ceiling, same as memcmp). Parity: conformance_diff_wcscmp.rs sign-matches host glibc wcscmp/wcsncmp across 50k+ pairs + a guard-page over-read test. Twelfth ABI string-vein win. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e555616 commit 3176c1f

2 files changed

Lines changed: 187 additions & 48 deletions

File tree

crates/frankenlibc-abi/src/wchar_abi.rs

Lines changed: 90 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,82 @@ pub unsafe extern "C" fn wcscat(dst: *mut u32, src: *const u32) -> *mut u32 {
534534
dst
535535
}
536536

537+
/// True iff a 32-byte read at `addr` stays within `addr`'s own 4096-byte page,
538+
/// so a wide dual-pointer vector load cannot fault past a NUL near a page
539+
/// boundary. Neither `s1` nor `s2` can be pre-aligned, hence the per-read guard.
540+
#[inline(always)]
541+
fn wide32_read_within_page(addr: usize) -> bool {
542+
(addr & 0xFFF) <= 0x1000 - 32
543+
}
544+
545+
/// Fused portable-SIMD wide-string compare: 8 `u32` (wchar_t) lanes per 32-byte
546+
/// window. `bound` is in elements. Returns `(result, span_elements, hit_limit)`:
547+
/// `result` is the signed difference (`-1`/`0`/`+1`, wchar_t compared as i32) at
548+
/// the first differing element or shared NUL; `hit_limit` means `bound` elements
549+
/// compared equal with no NUL. Equal-and-NUL-free windows advance 8 elements;
550+
/// others resolve element-wise (identical to the scalar loop). Wide reads are
551+
/// page-cross guarded (dual pointers can't be pre-aligned). 8 lanes per window
552+
/// amortise the guard cost — unlike a 2-lane u64-SWAR, which lost to scalar.
553+
unsafe fn scan_wcscmp_simd(s1: *const u32, s2: *const u32, bound: usize) -> (c_int, usize, bool) {
554+
const WLANES: usize = 8;
555+
let zv = Simd::<u32, WLANES>::splat(0);
556+
let mut i = 0usize;
557+
loop {
558+
if i + WLANES <= bound
559+
&& wide32_read_within_page(s1.wrapping_add(i) as usize)
560+
&& wide32_read_within_page(s2.wrapping_add(i) as usize)
561+
{
562+
// SAFETY: both 32-byte reads stay within their pages and within bound.
563+
// Raw array loads (not Rust slices over C memory) mirror wcschr.
564+
let va = Simd::<u32, WLANES>::from_array(unsafe {
565+
core::ptr::read(s1.add(i).cast::<[u32; WLANES]>())
566+
});
567+
let vb = Simd::<u32, WLANES>::from_array(unsafe {
568+
core::ptr::read(s2.add(i).cast::<[u32; WLANES]>())
569+
});
570+
if va == vb && !va.simd_eq(zv).any() {
571+
i += WLANES;
572+
continue;
573+
}
574+
for j in 0..WLANES {
575+
// SAFETY: i+j < bound.
576+
let a = unsafe { *s1.add(i + j) };
577+
let b = unsafe { *s2.add(i + j) };
578+
if a != b {
579+
return (if (a as i32) < (b as i32) { -1 } else { 1 }, i + j + 1, false);
580+
}
581+
if a == 0 {
582+
return (0, i + j + 1, false);
583+
}
584+
}
585+
i += WLANES; // defensive: a flagged window always returns above.
586+
continue;
587+
}
588+
if i >= bound {
589+
return (0, bound, true);
590+
}
591+
// SAFETY: i < bound.
592+
let a = unsafe { *s1.add(i) };
593+
let b = unsafe { *s2.add(i) };
594+
if a != b {
595+
return (if (a as i32) < (b as i32) { -1 } else { 1 }, i + 1, false);
596+
}
597+
if a == 0 {
598+
return (0, i + 1, false);
599+
}
600+
i += 1;
601+
}
602+
}
603+
604+
/// Benchmark/test hook for [`scan_wcscmp_simd`]. Not part of the public ABI.
605+
///
606+
/// # Safety
607+
/// `s1`/`s2` must be NUL-terminated, or valid for `bound` elements.
608+
#[doc(hidden)]
609+
pub unsafe fn bench_scan_wcscmp_simd(s1: *const u32, s2: *const u32, bound: usize) -> c_int {
610+
unsafe { scan_wcscmp_simd(s1, s2, bound).0 }
611+
}
612+
537613
// ---------------------------------------------------------------------------
538614
// wcscmp
539615
// ---------------------------------------------------------------------------
@@ -575,29 +651,12 @@ pub unsafe extern "C" fn wcscmp(s1: *const u32, s2: *const u32) -> c_int {
575651
(None, None) => None,
576652
};
577653

654+
// Fused portable-SIMD wide compare (shared scan_wcscmp_simd), byte-identical
655+
// to the old scalar element loop. `cmp_bound == None` => no limit; any
656+
// hit-limit is the membrane bound, so it maps directly to `adverse`.
578657
let (result, adverse, span) = unsafe {
579-
let mut i = 0usize;
580-
let mut adverse_local = false;
581-
loop {
582-
if let Some(limit) = cmp_bound
583-
&& i >= limit
584-
{
585-
adverse_local = true;
586-
break (0, adverse_local, i);
587-
}
588-
let a = *s1.add(i);
589-
let b = *s2.add(i);
590-
if a != b || a == 0 {
591-
// Cast to i32 for signed wchar_t comparison
592-
let diff = if (a as i32) < (b as i32) { -1 } else { 1 };
593-
break (
594-
if a == b { 0 } else { diff },
595-
adverse_local,
596-
i.saturating_add(1),
597-
);
598-
}
599-
i += 1;
600-
}
658+
let (r, span, hit_limit) = scan_wcscmp_simd(s1, s2, cmp_bound.unwrap_or(usize::MAX));
659+
(r, hit_limit, span)
601660
};
602661

603662
if adverse {
@@ -653,32 +712,15 @@ pub unsafe extern "C" fn wcsncmp(s1: *const u32, s2: *const u32, n: usize) -> c_
653712
(None, None) => Some(n),
654713
};
655714

715+
// Fused portable-SIMD wide compare (shared scan_wcscmp_simd); `cmp_bound` is
716+
// always Some here. `adverse` only when the limit came from a membrane clamp
717+
// (not n), matching the old scalar loop exactly.
718+
let limit = cmp_bound.expect("wcsncmp cmp_bound is always Some");
656719
let (result, adverse, span) = unsafe {
657-
let mut i = 0usize;
658-
let mut adverse_local = false;
659-
loop {
660-
if let Some(limit) = cmp_bound
661-
&& i >= limit
662-
{
663-
// Reached limit (n or bounds). If limit < n and limited by bounds, it's adverse.
664-
if limit < n && (lhs_bound == Some(limit) || rhs_bound == Some(limit)) {
665-
adverse_local = true;
666-
}
667-
break (0, adverse_local, i);
668-
}
669-
let a = *s1.add(i);
670-
let b = *s2.add(i);
671-
if a != b || a == 0 {
672-
// Cast to i32 for signed wchar_t comparison
673-
let diff = if (a as i32) < (b as i32) { -1 } else { 1 };
674-
break (
675-
if a == b { 0 } else { diff },
676-
adverse_local,
677-
i.saturating_add(1),
678-
);
679-
}
680-
i += 1;
681-
}
720+
let (r, span, hit_limit) = scan_wcscmp_simd(s1, s2, limit);
721+
let adverse_local =
722+
hit_limit && limit < n && (lhs_bound == Some(limit) || rhs_bound == Some(limit));
723+
(r, adverse_local, span)
682724
};
683725

684726
if adverse {
@@ -692,6 +734,7 @@ pub unsafe extern "C" fn wcsncmp(s1: *const u32, s2: *const u32, n: usize) -> c_
692734
);
693735
result
694736
}
737+
695738
/// Portable-SIMD scan of a NUL-terminated wide string for the first element equal
696739
/// to `c` OR the terminating NUL. Returns `(index, found_c)`; `c == 0` reports the
697740
/// NUL as a found match (matching `wcschr(s, '\0')`). Probes 8 `u32` lanes at a

crates/frankenlibc-bench/benches/memset_abi_bench.rs

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,31 @@ use frankenlibc_abi::string_abi::{
1515
bench_scan_c_string_for_byte, bench_scan_c_string_last_byte, bench_scan_strcasecmp,
1616
bench_scan_strcmp,
1717
};
18-
use frankenlibc_abi::wchar_abi::bench_wide_find_or_nul_simd;
18+
use frankenlibc_abi::wchar_abi::{bench_scan_wcscmp_simd, bench_wide_find_or_nul_simd};
19+
20+
/// Pre-lever wcscmp: scalar wchar_t (u32) element-at-a-time compare to diff/NUL.
21+
#[inline(never)]
22+
unsafe fn old_scalar_wcscmp(s1: *const u32, s2: *const u32) -> i32 {
23+
unsafe {
24+
let mut i = 0usize;
25+
loop {
26+
let a = *s1.add(i);
27+
let b = *s2.add(i);
28+
if a != b {
29+
return if (a as i32) < (b as i32) { -1 } else { 1 };
30+
}
31+
if a == 0 {
32+
return 0;
33+
}
34+
i += 1;
35+
}
36+
}
37+
}
1938

2039
unsafe extern "C" {
2140
fn wcschr(s: *const u32, c: u32) -> *mut u32;
41+
fn wcsrchr(s: *const u32, c: u32) -> *mut u32;
42+
fn wcscmp(s1: *const u32, s2: *const u32) -> i32;
2243
}
2344

2445
/// Pre-lever wcschr: scalar wchar_t (u32) scan to target-or-NUL.
@@ -39,6 +60,25 @@ unsafe fn old_scalar_wcschr(s: *const u32, c: u32) -> Option<usize> {
3960
}
4061
}
4162

63+
/// Pre-lever wcsrchr: scalar wchar_t (u32) scan to NUL, tracking last target.
64+
#[inline(never)]
65+
unsafe fn old_scalar_wcsrchr(s: *const u32, c: u32) -> Option<usize> {
66+
unsafe {
67+
let mut last = None;
68+
let mut i = 0usize;
69+
loop {
70+
let ch = *s.add(i);
71+
if ch == c {
72+
last = Some(i);
73+
}
74+
if ch == 0 {
75+
return last;
76+
}
77+
i += 1;
78+
}
79+
}
80+
}
81+
4282
/// Pre-lever strncpy copy+pad: byte-at-a-time copy to NUL then byte NUL-pad.
4383
#[inline(never)]
4484
unsafe fn old_byte_strncpy(dst: *mut u8, src: *const u8, n: usize) {
@@ -549,4 +589,60 @@ fn main() {
549589
gl / new,
550590
);
551591
}
592+
593+
println!("\nwcsrchr (absent target -> full wide scan to NUL):");
594+
println!(
595+
"{:>8} | {:>12} | {:>12} | {:>12}",
596+
"wchars", "old(ns)", "abi(ns)", "old/abi"
597+
);
598+
for &n in &sizes {
599+
let mut s: Vec<u32> = vec![0x61u32; n + 1];
600+
s[n] = 0;
601+
let p = s.as_ptr();
602+
let iters = (4_000_000u64 / (n as u64 + 1)).max(2000);
603+
604+
let old = median_ns_per_op(rounds, iters, || {
605+
black_box(unsafe { old_scalar_wcsrchr(p, 0x5A) });
606+
});
607+
let abi = median_ns_per_op(rounds, iters, || {
608+
// SAFETY: NUL-terminated wide string.
609+
black_box(unsafe { wcsrchr(p, 0x5A) });
610+
});
611+
println!(
612+
"{:>8} | {:>12.1} | {:>12.1} | {:>11.2}x",
613+
n,
614+
old,
615+
abi,
616+
old / abi,
617+
);
618+
}
619+
620+
println!("\nwcscmp (equal wide strings → full scan, wchar_t = u32):");
621+
println!(
622+
"{:>8} | {:>12} | {:>12} | {:>12} | {:>10} | {:>10}",
623+
"wchars", "old(ns)", "new(ns)", "glibc(ns)", "self x", "vs glibc"
624+
);
625+
for &n in &sizes {
626+
let mut a: Vec<u32> = vec![0x61u32; n + 1];
627+
a[n] = 0;
628+
let b = a.clone();
629+
let pa = a.as_ptr();
630+
let pb = b.as_ptr();
631+
let iters = (4_000_000u64 / (n as u64 + 1)).max(2000);
632+
633+
let old = median_ns_per_op(rounds, iters, || {
634+
black_box(unsafe { old_scalar_wcscmp(pa, pb) });
635+
});
636+
let new = median_ns_per_op(rounds, iters, || {
637+
black_box(unsafe { bench_scan_wcscmp_simd(pa, pb, usize::MAX) });
638+
});
639+
let gl = median_ns_per_op(rounds, iters, || {
640+
// SAFETY: both NUL-terminated wide strings.
641+
black_box(unsafe { wcscmp(pa, pb) });
642+
});
643+
println!(
644+
"{:>8} | {:>12.1} | {:>12.1} | {:>12.1} | {:>9.2}x | {:>9.2}x",
645+
n, old, new, gl, old / new, gl / new,
646+
);
647+
}
552648
}

0 commit comments

Comments
 (0)