4 个版本
0.1.2 | 2022年11月26日 |
---|---|
0.1.1 | 2022年11月26日 |
0.1.0 | 2022年11月26日 |
#547 在 文件系统
28 每月下载量
用于 working_dir
4MB
462 行
path_no_alloc:一个用于栈分配路径的舒适库
最近我开始开发一个非常依赖路径操作的库。在与充斥着 Path.join 调用的函数打交道之后,我想要一个更快、更舒适的解决方案。肯定有更好的方法,对吧?
这就是 path_no_alloc
的作用。它是一个简单的库,用于连接和使用路径,同时避免对小于 128 字节的短路径进行分配。
使用非常简单。在 with_paths!
内部,路径使用 /
运算符连接,如果路径的总长度小于 128 字节,操作将在栈分配的缓冲区内部进行。
use path_no_alloc::with_paths;
let p1 = "hello";
let p2 = "world";
// Here, we create a variable `path` of type `&Path` that
// represents p1 joined to p2
with_paths! {
path = p1 / p2
};
assert_eq!(path, std::path::Path::new("hello/world"));
with_paths!
也可以用作复合表达式,通过使用 =>
运算符在声明后附加语句
use path_no_alloc::with_paths;
let p1 = "some/dir";
let p2 = "file.txt";
let file_exists: bool = with_paths! {
path = p1 / p2 => path.exists()
};
在 with_paths!
块内部可以有无限数量的语句,也可以创建和连接任意数量的路径。任何实现了 AsRef<Path>
的类型都可以用于声明并连接到其他路径。
use path_no_alloc::with_paths;
use std::path::{Path, PathBuf};
let p1 = Path::new("hello");
let p2 = "world".to_string();
let my_file = "file.txt";
let some_root = PathBuf::from("some/project/root");
let path_exists = with_paths! {
path = p1 / p2,
another_path = some_root / p1 / my_file,
some_third_path = p1 / p2 / some_root / my_file
=>
assert_eq!(path, std::path::Path::new("hello/world"));
assert_eq!(another_path, std::path::Path::new("some/project/root/hello/file.txt"));
let path_exists = some_third_path.exists();
println!("{some_third_path:?} exists? {path_exists}");
path_exists
};
最后,在 with_paths!
块中连接的路径的行为与使用 Path.join
连接的路径完全相同。这意味着将绝对路径与其他路径连接将截断并仅返回绝对路径
use path_no_alloc::with_paths;
use std::path::Path;
let working_dir = "my/working/dir";
let rel_path = "path/to/file.txt";
let abs_path = "/path/to/file.txt";
with_paths! {
relative = working_dir / rel_path,
absolute = working_dir / abs_path
};
// Joining a relative path appends it
assert_eq!(relative, Path::new("my/working/dir/path/to/file.txt"));
// But joining an absolute path just results in the absolute path
assert_eq!(absolute, Path::new("/path/to/file.txt"));
// this is the same as the behavior of Path.join():
assert_eq!(relative, Path::new(working_dir).join(rel_path));
assert_eq!(absolute, Path::new(working_dir).join(abs_path));
细节
性能
在测试了 131072 个随机路径后,使用 with_paths!
宏比调用 Path.join()
快 2-3.5 倍,且这是在理想条件下 Path.join
。分配本质上是非确定的;它受到多个线程的竞争;并且在某些特定延迟的应用程序上下文中必须完全避免。
在实际情况下,使用 with_paths!
可以避免大多数情况下的分配(我假设处理超过 128 个字符的路径并不常见)。
即使有必要进行系统调用(例如检查文件是否存在),使用 with_paths!
仍然可以实现 20-30% 的性能提升。
(如果这些图像没有在 crate 文档中显示,请访问 github.com/codeinred/path_no_alloc
上图显示了使用Path.join
连接两个随机选择的路径所需时间与使用with_paths!
连接两个路径所需时间的比较。X轴表示两个随机选择的路径的平均总长度。从平均路径长度为64开始,两个随机选择的路径总长度大于128的概率不为零,导致发生分配。
上图显示了与折线图相同数据的提琴图。基准ID中的最后一个数字对应于两个随机选择的路径的平均总长度。
您测试了边缘情况吗?
是的。以下所有边缘情况都已测试:
- 连接1个或多个路径,其中一个为绝对路径
- 连接1个或多个路径,其中一些或所有路径为空
- 连接路径,其中结果路径正好与缓冲区大小相同
还有额外的模糊测试,其中测试了10个随机路径的组合,其中一些路径可能为空,或为绝对路径,路径长度可能超过栈缓冲区大小。
请参阅tests.rs
如果路径不适合栈缓冲区会发生什么?
如果路径不适合栈缓冲区,那么with_paths!
将计算所有路径的总长度;预留适当大小的PathBuf
;并使用该PathBuf
连接路径。此操作对用户是完全透明的,在进行路径操作时,您仍然使用&Path
。
请注意,这仍然比Path::new(a).join(b)
更有效,因为典型的对Path.join
的调用将导致2次分配
- 首先,它将创建包含
a
的PathBuf, - 然后它将
b
推送到路径buf。
不幸的是,Path.join
没有预先分配足够的空间,导致第二次分配。
我希望为此问题向标准库提交一个错误修复。
为什么我写了path_no_alloc
不幸的是,除非您的应用程序正在进行大量路径操作,否则使用with_paths!
不会在全球范围内产生有意义的性能提升。通常,连接两个路径所需的时间被用于结果路径的操作所淹没 - 文件创建、IO、读写或其他与OS /文件系统的交互。
我写这个库主要是出于好奇心,并且我发现这个接口比到处调用Path.join
更好。这是我第一次尝试Rust中的未初始化内存和栈分配缓冲区,我确实学到了很多。
编写避免分配的路径操作是一个挑战,但我发现这很有趣。
在C++等语言中,栈分配的缓冲区可能会让人头疼,尤其是大多数情况下程序员只是简单地认为缓冲区足够——如果栈缓冲区溢出,那就糟糕了!Rust的卫生宏使得栈分配的缓冲区可以安全、高效地使用,如果缓冲区溢出,将自动回退到堆分配的内存,并且它们允许你以对程序员实际透明的的方式进行。使用with_paths!
,我不会增加额外的心理负担,因为它的语法比Path.join
更简洁、更直接!
话虽如此,我希望你可能会觉得这个库很有用,或者至少是有教育意义的。
充满爱意,
—— Alecto