Skip to content

Commit 1b22541

Browse files
committed
Auto merge of rust-lang#93668 - SUPERCILEX:path_alloc, r=joshtriplett
Reduce CString allocations in std as much as possible Currently, every operation involving paths in `fs` allocates memory to hold the path before sending it through the syscall. This PR instead uses a stack allocation (chosen size is somewhat arbitrary) when the path is short before falling back to heap allocations for long paths. Benchmarks show that the stack allocation is ~2x faster for short paths: ``` test sys::unix::fd::tests::bench_heap_path_alloc ... bench: 34 ns/iter (+/- 2) test sys::unix::fd::tests::bench_stack_path_alloc ... bench: 15 ns/iter (+/- 1) ``` For long paths, I couldn't find any measurable difference. --- I'd be surprised if I was the first to think of this, so I didn't fully flush out the PR. If this change is desirable, I'll make use of `run_with_cstr` across all platforms in every fs method (currently just unix open for testing). I also added an `impl From<FromBytesWithNulError>` which is presumably a no-no (or at least needs to be done in another PR). --- Also see nix-rust/nix#1655 with a bunch of discussion where I'm doing something similar.
2 parents 79a664d + 86974b8 commit 1b22541

File tree

13 files changed

+397
-286
lines changed

13 files changed

+397
-286
lines changed

library/std/src/sys/common/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@
1111
#![allow(dead_code)]
1212

1313
pub mod alloc;
14+
pub mod small_c_string;
15+
16+
#[cfg(test)]
17+
mod tests;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use crate::ffi::{CStr, CString};
2+
use crate::mem::MaybeUninit;
3+
use crate::path::Path;
4+
use crate::slice;
5+
use crate::{io, ptr};
6+
7+
// Make sure to stay under 4096 so the compiler doesn't insert a probe frame:
8+
// https://docs.rs/compiler_builtins/latest/compiler_builtins/probestack/index.html
9+
#[cfg(not(target_os = "espidf"))]
10+
const MAX_STACK_ALLOCATION: usize = 384;
11+
#[cfg(target_os = "espidf")]
12+
const MAX_STACK_ALLOCATION: usize = 32;
13+
14+
const NUL_ERR: io::Error =
15+
io::const_io_error!(io::ErrorKind::InvalidInput, "file name contained an unexpected NUL byte");
16+
17+
#[inline]
18+
pub fn run_path_with_cstr<T, F>(path: &Path, f: F) -> io::Result<T>
19+
where
20+
F: FnOnce(&CStr) -> io::Result<T>,
21+
{
22+
run_with_cstr(path.as_os_str().bytes(), f)
23+
}
24+
25+
#[inline]
26+
pub fn run_with_cstr<T, F>(bytes: &[u8], f: F) -> io::Result<T>
27+
where
28+
F: FnOnce(&CStr) -> io::Result<T>,
29+
{
30+
if bytes.len() >= MAX_STACK_ALLOCATION {
31+
return run_with_cstr_allocating(bytes, f);
32+
}
33+
34+
let mut buf = MaybeUninit::<[u8; MAX_STACK_ALLOCATION]>::uninit();
35+
let buf_ptr = buf.as_mut_ptr() as *mut u8;
36+
37+
unsafe {
38+
ptr::copy_nonoverlapping(bytes.as_ptr(), buf_ptr, bytes.len());
39+
buf_ptr.add(bytes.len()).write(0);
40+
}
41+
42+
match CStr::from_bytes_with_nul(unsafe { slice::from_raw_parts(buf_ptr, bytes.len() + 1) }) {
43+
Ok(s) => f(s),
44+
Err(_) => Err(NUL_ERR),
45+
}
46+
}
47+
48+
#[cold]
49+
#[inline(never)]
50+
fn run_with_cstr_allocating<T, F>(bytes: &[u8], f: F) -> io::Result<T>
51+
where
52+
F: FnOnce(&CStr) -> io::Result<T>,
53+
{
54+
match CString::new(bytes) {
55+
Ok(s) => f(&s),
56+
Err(_) => Err(NUL_ERR),
57+
}
58+
}

library/std/src/sys/common/tests.rs

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use crate::ffi::CString;
2+
use crate::hint::black_box;
3+
use crate::path::Path;
4+
use crate::sys::common::small_c_string::run_path_with_cstr;
5+
use core::iter::repeat;
6+
7+
#[test]
8+
fn stack_allocation_works() {
9+
let path = Path::new("abc");
10+
let result = run_path_with_cstr(path, |p| {
11+
assert_eq!(p, &*CString::new(path.as_os_str().bytes()).unwrap());
12+
Ok(42)
13+
});
14+
assert_eq!(result.unwrap(), 42);
15+
}
16+
17+
#[test]
18+
fn stack_allocation_fails() {
19+
let path = Path::new("ab\0");
20+
assert!(run_path_with_cstr::<(), _>(path, |_| unreachable!()).is_err());
21+
}
22+
23+
#[test]
24+
fn heap_allocation_works() {
25+
let path = repeat("a").take(384).collect::<String>();
26+
let path = Path::new(&path);
27+
let result = run_path_with_cstr(path, |p| {
28+
assert_eq!(p, &*CString::new(path.as_os_str().bytes()).unwrap());
29+
Ok(42)
30+
});
31+
assert_eq!(result.unwrap(), 42);
32+
}
33+
34+
#[test]
35+
fn heap_allocation_fails() {
36+
let mut path = repeat("a").take(384).collect::<String>();
37+
path.push('\0');
38+
let path = Path::new(&path);
39+
assert!(run_path_with_cstr::<(), _>(path, |_| unreachable!()).is_err());
40+
}
41+
42+
#[bench]
43+
fn bench_stack_path_alloc(b: &mut test::Bencher) {
44+
let path = repeat("a").take(383).collect::<String>();
45+
let p = Path::new(&path);
46+
b.iter(|| {
47+
run_path_with_cstr(p, |cstr| {
48+
black_box(cstr);
49+
Ok(())
50+
})
51+
.unwrap();
52+
});
53+
}
54+
55+
#[bench]
56+
fn bench_heap_path_alloc(b: &mut test::Bencher) {
57+
let path = repeat("a").take(384).collect::<String>();
58+
let p = Path::new(&path);
59+
b.iter(|| {
60+
run_path_with_cstr(p, |cstr| {
61+
black_box(cstr);
62+
Ok(())
63+
})
64+
.unwrap();
65+
});
66+
}

library/std/src/sys/hermit/fs.rs

+4-9
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
use crate::convert::TryFrom;
12
use crate::ffi::{CStr, CString, OsString};
23
use crate::fmt;
34
use crate::hash::{Hash, Hasher};
45
use crate::io::{self, Error, ErrorKind};
56
use crate::io::{BorrowedCursor, IoSlice, IoSliceMut, SeekFrom};
67
use crate::os::unix::ffi::OsStrExt;
78
use crate::path::{Path, PathBuf};
9+
use crate::sys::common::small_c_string::run_path_with_cstr;
810
use crate::sys::cvt;
911
use crate::sys::hermit::abi;
1012
use crate::sys::hermit::abi::{O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY};
@@ -15,10 +17,6 @@ use crate::sys::unsupported;
1517
pub use crate::sys_common::fs::{copy, try_exists};
1618
//pub use crate::sys_common::fs::remove_dir_all;
1719

18-
fn cstr(path: &Path) -> io::Result<CString> {
19-
Ok(CString::new(path.as_os_str().as_bytes())?)
20-
}
21-
2220
#[derive(Debug)]
2321
pub struct File(FileDesc);
2422

@@ -272,8 +270,7 @@ impl OpenOptions {
272270

273271
impl File {
274272
pub fn open(path: &Path, opts: &OpenOptions) -> io::Result<File> {
275-
let path = cstr(path)?;
276-
File::open_c(&path, opts)
273+
run_path_with_cstr(path, |path| File::open_c(&path, opts))
277274
}
278275

279276
pub fn open_c(path: &CStr, opts: &OpenOptions) -> io::Result<File> {
@@ -373,9 +370,7 @@ pub fn readdir(_p: &Path) -> io::Result<ReadDir> {
373370
}
374371

375372
pub fn unlink(path: &Path) -> io::Result<()> {
376-
let name = cstr(path)?;
377-
let _ = unsafe { cvt(abi::unlink(name.as_ptr()))? };
378-
Ok(())
373+
run_path_with_cstr(path, |path| cvt(unsafe { abi::unlink(path.as_ptr()) }).map(|_| ()))
379374
}
380375

381376
pub fn rename(_old: &Path, _new: &Path) -> io::Result<()> {

library/std/src/sys/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
2323
#![allow(missing_debug_implementations)]
2424

25-
mod common;
25+
pub mod common;
2626

2727
cfg_if::cfg_if! {
2828
if #[cfg(unix)] {

library/std/src/sys/solid/os.rs

+20-20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::unsupported;
2+
use crate::convert::TryFrom;
23
use crate::error::Error as StdError;
34
use crate::ffi::{CStr, CString, OsStr, OsString};
45
use crate::fmt;
@@ -9,6 +10,7 @@ use crate::os::{
910
};
1011
use crate::path::{self, PathBuf};
1112
use crate::sync::RwLock;
13+
use crate::sys::common::small_c_string::run_with_cstr;
1214
use crate::vec;
1315

1416
use super::{error, itron, memchr};
@@ -139,35 +141,33 @@ pub fn env() -> Env {
139141
pub fn getenv(k: &OsStr) -> Option<OsString> {
140142
// environment variables with a nul byte can't be set, so their value is
141143
// always None as well
142-
let k = CString::new(k.as_bytes()).ok()?;
143-
unsafe {
144+
let s = run_with_cstr(k.as_bytes(), |k| {
144145
let _guard = ENV_LOCK.read();
145-
let s = libc::getenv(k.as_ptr()) as *const libc::c_char;
146-
if s.is_null() {
147-
None
148-
} else {
149-
Some(OsStringExt::from_vec(CStr::from_ptr(s).to_bytes().to_vec()))
150-
}
146+
Ok(unsafe { libc::getenv(k.as_ptr()) } as *const libc::c_char)
147+
})
148+
.ok()?;
149+
150+
if s.is_null() {
151+
None
152+
} else {
153+
Some(OsStringExt::from_vec(unsafe { CStr::from_ptr(s) }.to_bytes().to_vec()))
151154
}
152155
}
153156

154157
pub fn setenv(k: &OsStr, v: &OsStr) -> io::Result<()> {
155-
let k = CString::new(k.as_bytes())?;
156-
let v = CString::new(v.as_bytes())?;
157-
158-
unsafe {
159-
let _guard = ENV_LOCK.write();
160-
cvt_env(libc::setenv(k.as_ptr(), v.as_ptr(), 1)).map(drop)
161-
}
158+
run_with_cstr(k.as_bytes(), |k| {
159+
run_with_cstr(v.as_bytes(), |v| {
160+
let _guard = ENV_LOCK.write();
161+
cvt_env(unsafe { libc::setenv(k.as_ptr(), v.as_ptr(), 1) }).map(drop)
162+
})
163+
})
162164
}
163165

164166
pub fn unsetenv(n: &OsStr) -> io::Result<()> {
165-
let nbuf = CString::new(n.as_bytes())?;
166-
167-
unsafe {
167+
run_with_cstr(n.as_bytes(), |nbuf| {
168168
let _guard = ENV_LOCK.write();
169-
cvt_env(libc::unsetenv(nbuf.as_ptr())).map(drop)
170-
}
169+
cvt_env(unsafe { libc::unsetenv(nbuf.as_ptr()) }).map(drop)
170+
})
171171
}
172172

173173
/// In kmclib, `setenv` and `unsetenv` don't always set `errno`, so this

0 commit comments

Comments
 (0)