#path #fs

path_no_alloc

提供 with_paths! 宏,以舒适且不进行分配的方式连接路径

4 个版本

0.1.2 2022年11月26日
0.1.1 2022年11月26日
0.1.0 2022年11月26日

#547文件系统

28 每月下载量
用于 working_dir

MIT 许可证

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

无运行时依赖