Skip to content

Commit 7c0d053

Browse files
perf(strnlen): widen scan_c_string bounded NUL scan 8B SWAR -> 32B portable-SIMD
strnlen rode the 8-byte SWAR bounded path of scan_c_string while strlen has a dedicated 64-lane SIMD dispatch — leaving strnlen ~1.79x slower than glibc's strnlen. Added a 32-byte portable-SIMD NUL scan at the top of the bounded loop: bounded mode guarantees `limit` readable bytes, so a Simd<u8,32> load is in-bounds whenever i+32 <= limit (no page check needed). NUL-free panels advance 32; a panel containing a NUL breaks to the existing 8-byte SWAR / scalar tail, which returns the exact NUL index — so the result is identical (isomorphic by construction). This also accelerates the bounded/repair-path length scans of strcmp/strcasecmp/ strncmp/strncasecmp that share scan_c_string. Before/after (strnlen 4K, ratio = fl / glibc): 1.79x -> 1.26x. Isomorphism + golden: conformance_diff_strnlen_simd runs 300000 random (buffer, NUL-position, n) triples — n straddling the 32-byte panel, NULs at every offset — with 0 divergences vs host glibc strnlen and a pinned sha256 of the length stream. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7f1fdb0 commit 7c0d053

2 files changed

Lines changed: 73 additions & 0 deletions

File tree

crates/frankenlibc-abi/src/string_abi.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,22 @@ unsafe fn scan_c_string(ptr: *const c_char, bound: Option<usize>) -> (usize, boo
960960
match bound {
961961
Some(limit) => {
962962
let mut i = 0usize;
963+
// Wide 32-byte portable-SIMD NUL scan (AVX width, like glibc's
964+
// strnlen). Bounded mode guarantees `limit` readable bytes, so a
965+
// 32-byte load is in-bounds whenever i+32 <= limit. NUL-free panels
966+
// advance 32; a panel containing a NUL drops to the 8-byte SWAR /
967+
// scalar tail below, which returns the exact NUL index unchanged.
968+
while i + 32 <= limit {
969+
use core::simd::Simd;
970+
use core::simd::cmp::SimdPartialEq;
971+
// SAFETY: [i, i+32) ⊆ [0, limit); `limit` bytes are readable.
972+
let v =
973+
Simd::<u8, 32>::from_slice(unsafe { core::slice::from_raw_parts(p.add(i), 32) });
974+
if v.simd_eq(Simd::splat(0)).any() {
975+
break;
976+
}
977+
i += 32;
978+
}
963979
while i + 8 <= limit {
964980
// SAFETY: [i, i+8) ⊆ [0, limit); caller guarantees `limit` readable bytes.
965981
let w = unsafe { core::ptr::read_unaligned(p.add(i).cast::<u64>()) };
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#![cfg(target_os = "linux")]
2+
#![allow(unsafe_code)]
3+
//! Isomorphism + golden gate for the 32-byte portable-SIMD NUL scan added to the
4+
//! bounded path of `scan_c_string` (used by strnlen and the bounded/repair-path
5+
//! length scans of strcmp/strcasecmp/strncmp/strncasecmp). Widened from 8-byte
6+
//! SWAR to AVX width to close strnlen's ~1.79x throughput gap vs glibc.
7+
//! 300000 random (buffer, NUL position, n) triples — with n straddling the
8+
//! 32-byte panel and NULs at every offset — agree exactly with host glibc
9+
//! strnlen; a golden sha256 of the length stream pins the behavior.
10+
11+
use std::os::raw::c_char;
12+
use frankenlibc_abi::string_abi as fa;
13+
use sha2::{Digest, Sha256};
14+
15+
unsafe extern "C" {
16+
fn strnlen(s: *const c_char, n: usize) -> usize;
17+
}
18+
19+
#[test]
20+
fn strnlen_matches_glibc() {
21+
let mut seed: u64 = 0x1357;
22+
let mut rng = || {
23+
seed ^= seed << 13;
24+
seed ^= seed >> 7;
25+
seed ^= seed << 17;
26+
seed
27+
};
28+
let mut h = Sha256::new();
29+
let mut div = 0u32;
30+
for _ in 0..300000 {
31+
let buflen = (rng() as usize) % 140;
32+
let mut buf: Vec<u8> = (0..buflen).map(|_| ((rng() % 90) + 33) as u8).collect();
33+
if rng() & 1 == 0 && buflen > 0 {
34+
let k = (rng() as usize) % buflen;
35+
buf[k] = 0;
36+
}
37+
buf.push(0); // guaranteed terminator
38+
let n = (rng() as usize) % (buflen + 40);
39+
let fl = unsafe { fa::strnlen(buf.as_ptr() as *const c_char, n) };
40+
let gl = unsafe { strnlen(buf.as_ptr() as *const c_char, n) };
41+
if fl != gl {
42+
div += 1;
43+
if div <= 5 {
44+
eprintln!("DIV n={n} buflen={buflen} fl={fl} gl={gl}");
45+
}
46+
}
47+
h.update((fl as u64).to_le_bytes());
48+
}
49+
let hex: String = h.finalize().iter().map(|b| format!("{b:02x}")).collect();
50+
eprintln!("strnlen golden sha256: {hex}");
51+
assert_eq!(div, 0, "strnlen diverged from glibc in {div} cases");
52+
assert_eq!(
53+
hex,
54+
"fc35325dad341a14ff0be7d64af0a429f9695a9d4ae7d5e8aed982e040848efe",
55+
"strnlen golden changed"
56+
);
57+
}

0 commit comments

Comments
 (0)