Skip to content

Commit faffed3

Browse files
perf(strncpy): SWAR scan + wide copy + wide pad for strncpy/stpncpy (bd-2g7oyh.363)
strncpy copied byte-at-a-time to the NUL then byte-padded the remainder with NUL. Replace the copy+pad loop with a composition of the already-shipped, independently-gated SWAR/wide primitives: scan_c_string (SWAR) finds the source NUL within the bound, raw_memcpy_bytes (wide u128-block) copies the prefix clamped to the dst bound, and raw_memset_bytes (wide) NUL-fills the remainder — byte-identical to the scalar loop including the membrane-repair clamping (safe_src_len/safe_dst_len). stpncpy delegates to strncpy + strnlen, so it inherits this and the SWAR strnlen win. Measured (rch, median ns/op, copy-heavy strlen==n full copy, no pad): n old(ns) new(ns) glibc(ns) self vs-glibc 64 33.4 7.6 20.9 4.39x 2.76x 256 116.0 20.7 34.1 5.60x 1.64x 1024 449.0 77.5 92.6 5.80x 1.20x 4096 1783.0 291.9 308.0 6.11x 1.06x 65536 28402.6 4524.2 4531.0 6.28x 1.00x Self-speedup 4.4-6.3x for n>=64 (>=2.0); matches-or-beats glibc strncpy at every size. (Pad-heavy strncpy was already fast — the byte pad lowered to memset — so this is the copy-portion win; total strncpy never slower.) Parity: conformance_diff_strncpy.rs proves fl strncpy/stpncpy produce byte- identical destination buffers (full n bytes incl. NUL padding) + stpncpy end offset vs host glibc across 2400 cases (src len 0..300 x n straddling the NUL x src/dst alignments 0..7). The composed primitives stay independently gated by conformance_diff_{scan_c_string,memcpy,memset}. Tenth ABI string-vein win. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e343836 commit faffed3

3 files changed

Lines changed: 140 additions & 12 deletions

File tree

crates/frankenlibc-abi/src/string_abi.rs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2651,19 +2651,18 @@ pub unsafe extern "C" fn strncpy(dst: *mut c_char, src: *const c_char, n: usize)
26512651
}
26522652

26532653
// SAFETY: bounded by safe_dst_len and safe_src_len.
2654+
// SWAR scan for the NUL, then a wide block copy of the prefix and a wide NUL
2655+
// pad of the remainder — composing the proven scan_c_string / raw_memcpy_bytes
2656+
// / raw_memset_bytes primitives instead of the byte-at-a-time copy+pad loop.
2657+
// `k` is the source NUL index (or safe_src_len if none within bound); the copy
2658+
// is clamped to safe_dst_len, and everything after it is NUL-filled — exactly
2659+
// what the scalar loop produced.
26542660
unsafe {
2655-
let mut i = 0usize;
2656-
while i < safe_dst_len {
2657-
let ch = if i < safe_src_len { *src.add(i) } else { 0 };
2658-
*dst.add(i) = ch;
2659-
i += 1;
2660-
if ch == 0 {
2661-
break;
2662-
}
2663-
}
2664-
while i < safe_dst_len {
2665-
*dst.add(i) = 0;
2666-
i += 1;
2661+
let k = scan_c_string(src, Some(safe_src_len)).0;
2662+
let copy_len = k.min(safe_dst_len);
2663+
raw_memcpy_bytes(dst.cast::<u8>(), src.cast::<u8>(), copy_len);
2664+
if copy_len < safe_dst_len {
2665+
raw_memset_bytes(dst.add(copy_len).cast::<u8>(), 0, safe_dst_len - copy_len);
26672666
}
26682667
}
26692668
runtime_policy::observe(
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//! Differential gate for the public `strncpy`/`stpncpy` ABI after replacing the
2+
//! byte-at-a-time copy+pad loop with SWAR scan + wide block copy + wide NUL pad.
3+
//! fl must produce byte-identical destination buffers (full n bytes, incl. the
4+
//! NUL padding) to host glibc across source lengths, `n` straddling the NUL, and
5+
//! both source and destination alignments — the regime where the wide copy and
6+
//! wide pad replace the old scalar loop.
7+
#![cfg(target_os = "linux")]
8+
#![allow(unsafe_code)]
9+
10+
use frankenlibc_abi::string_abi::{stpncpy as fl_stpncpy, strncpy as fl_strncpy};
11+
use std::os::raw::c_char;
12+
13+
#[test]
14+
fn strncpy_stpncpy_match_glibc() {
15+
let mut checked = 0u64;
16+
let lengths = [0usize, 1, 7, 8, 15, 16, 17, 31, 33, 63, 64, 100, 128, 255, 300];
17+
let ns = [0usize, 1, 8, 16, 33, 64, 100, 128, 256, 301];
18+
19+
for &src_off in &[0usize, 1, 3, 7] {
20+
for &dst_off in &[0usize, 1, 3, 7] {
21+
for &len in &lengths {
22+
// src: `len` non-NUL bytes (incl high-bit) then NUL.
23+
let mut src_buf = vec![0u8; src_off + len + 1];
24+
for k in 0..len {
25+
let b = (k as u8).wrapping_mul(53).wrapping_add(1);
26+
src_buf[src_off + k] = if b == 0 { 0x80 } else { b };
27+
}
28+
src_buf[src_off + len] = 0;
29+
let src = unsafe { src_buf.as_ptr().add(src_off) } as *const c_char;
30+
31+
for &n in &ns {
32+
// Destination buffers preset to 0xAA so any unwritten byte shows.
33+
let mut fl = vec![0xAAu8; dst_off + n + 1];
34+
let mut gl = vec![0xAAu8; dst_off + n + 1];
35+
let fl_end = unsafe { fl_strncpy(fl.as_mut_ptr().add(dst_off) as *mut c_char, src, n) };
36+
let gl_ret = unsafe {
37+
libc::strncpy(gl.as_mut_ptr().add(dst_off) as *mut c_char, src, n)
38+
};
39+
let _ = (fl_end, gl_ret);
40+
assert_eq!(
41+
fl, gl,
42+
"strncpy src_off={src_off} dst_off={dst_off} len={len} n={n}"
43+
);
44+
45+
// stpncpy: same buffer result + the returned end offset.
46+
let mut fl2 = vec![0xAAu8; dst_off + n + 1];
47+
let mut gl2 = vec![0xAAu8; dst_off + n + 1];
48+
let fl_base = unsafe { fl2.as_mut_ptr().add(dst_off) };
49+
let gl_base = unsafe { gl2.as_mut_ptr().add(dst_off) };
50+
let fe = unsafe { fl_stpncpy(fl_base as *mut c_char, src, n) };
51+
let ge = unsafe { libc::stpncpy(gl_base as *mut c_char, src, n) };
52+
assert_eq!(fl2, gl2, "stpncpy buf src_off={src_off} dst_off={dst_off} len={len} n={n}");
53+
assert_eq!(
54+
fe as usize - fl_base as usize,
55+
ge as usize - gl_base as usize,
56+
"stpncpy end off src_off={src_off} dst_off={dst_off} len={len} n={n}"
57+
);
58+
checked += 1;
59+
}
60+
}
61+
}
62+
}
63+
assert!(checked >= 2400, "corpus unexpectedly small: {checked}");
64+
}

crates/frankenlibc-bench/benches/memset_abi_bench.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,39 @@ use frankenlibc_abi::string_abi::{
1616
bench_scan_strcmp,
1717
};
1818

19+
/// Pre-lever strncpy copy+pad: byte-at-a-time copy to NUL then byte NUL-pad.
20+
#[inline(never)]
21+
unsafe fn old_byte_strncpy(dst: *mut u8, src: *const u8, n: usize) {
22+
unsafe {
23+
let mut i = 0usize;
24+
while i < n {
25+
let ch = *src.add(i);
26+
*dst.add(i) = ch;
27+
i += 1;
28+
if ch == 0 {
29+
break;
30+
}
31+
}
32+
while i < n {
33+
*dst.add(i) = 0;
34+
i += 1;
35+
}
36+
}
37+
}
38+
39+
/// New-lever strncpy: SWAR scan + wide copy + wide pad (the shipped composition).
40+
#[inline(never)]
41+
unsafe fn new_strncpy(dst: *mut u8, src: *const u8, n: usize) {
42+
unsafe {
43+
let k = bench_scan_c_string(src.cast::<std::os::raw::c_char>(), Some(n)).0;
44+
let copy_len = k.min(n);
45+
bench_raw_memcpy_bytes(dst, src, copy_len);
46+
if copy_len < n {
47+
bench_raw_memset_bytes(dst.add(copy_len), 0, n - copy_len);
48+
}
49+
}
50+
}
51+
1952
/// Pre-lever strncasecmp scan: byte-at-a-time tolower compare to first diff/NUL.
2053
#[inline(never)]
2154
unsafe fn old_byte_strcasecmp(
@@ -380,4 +413,36 @@ fn main() {
380413
n, old, new, gl, old / new, gl / new,
381414
);
382415
}
416+
417+
println!("\nstrncpy (copy-heavy: strlen==n, full copy no pad):");
418+
println!(
419+
"{:>8} | {:>12} | {:>12} | {:>12} | {:>10} | {:>10}",
420+
"n", "old(ns)", "new(ns)", "glibc(ns)", "self x", "vs glibc"
421+
);
422+
for &n in &sizes {
423+
let mut src = vec![0x61u8; n + 1];
424+
src[n] = 0;
425+
let mut dst = vec![0u8; n + 1];
426+
let sp = src.as_ptr();
427+
let dp = dst.as_mut_ptr();
428+
let iters = (4_000_000u64 / (n as u64 + 1)).max(2000);
429+
430+
let old = median_ns_per_op(rounds, iters, || {
431+
unsafe { old_byte_strncpy(dp, sp, n) };
432+
black_box(dst[0]);
433+
});
434+
let new = median_ns_per_op(rounds, iters, || {
435+
unsafe { new_strncpy(dp, sp, n) };
436+
black_box(dst[0]);
437+
});
438+
let gl = median_ns_per_op(rounds, iters, || {
439+
// SAFETY: src NUL-terminated, dst valid for n bytes.
440+
unsafe { libc::strncpy(dp.cast::<std::os::raw::c_char>(), sp.cast::<std::os::raw::c_char>(), n) };
441+
black_box(dst[0]);
442+
});
443+
println!(
444+
"{:>8} | {:>12.1} | {:>12.1} | {:>12.1} | {:>9.2}x | {:>9.2}x",
445+
n, old, new, gl, old / new, gl / new,
446+
);
447+
}
383448
}

0 commit comments

Comments
 (0)