ITADN
folo-rs/folo
README.md
以下内容由 AI 翻译,如有问题请点此提交 issue 反馈

Folo

用于 Rust 中高性能硬件感知编程的机制。

它为你提供了什么

要利用硬件感知能力,我们首先必须获得这种感知能力。 many_cpus 告知我们系统处理器的特性以及 它们相对于主内存的布局,从而让我们能够控制 特定逻辑在哪些特定处理器上运行。我们不再简单地盲目地 thread::spawn()

  • 现在我们为特定处理器或处理器组生成特定的线程。

many_cpus_benchmarking 提供了一个基准测试框架,用于探索当应用于不同算法时, 工作负载和数据在处理器之间分布方式所产生的影响。 这使您能够判断在何种情况下这很重要,以及影响程度有多大。

vicinal 允许您在同一处理器上调度同步任务,提供了一种 简单的方法来保持数据局部性,适用于必须从主应用程序线程中分离出来的工作 (例如,因为它是阻塞代码,不适合用于异步应用程序线程)。

将工作负载定向到特定处理器和特定内存区域的能力,解锁了新的优化机会。 region_localregion_cached 在处理器和主内存之间提供了一层类似缓存的机制,确保 即使无法放入处理器缓存的数据集,也能获得高数据局部性。

为硬件效率设计代码通常受益于线程隔离的思维模式,将每个线程视为其自身的宇宙。linked 提供了有价值的概念、隐喻和机制,使对象能够向每个线程呈现独特的面貌,在每个线程上作为独立的对象运行,同时通过内部逻辑相互连接,并仅允许跨线程边界的显式转移。这些是用于实现 region_localregion_cached 的构建模块。

测量硬件感知编程的效果有时需要基准测试是多线程的,而像 Criterion 这样的基准测试框架并不开箱即用地提供此功能。 par_bench 扩展了 Criterion,提供了一个用于多线程基准测试的简单框架,在从 many_cpus 获取的特定处理器集上运行你的基准测试逻辑。它负责处理协调线程和从数据中消除任何测试框架开销所涉及的所有繁琐工作。

Processor time statistics:

| Operation                   | Mean |
|-----------------------------|------|
| futures_oneshot_channel_mt  | 92ns |
| futures_oneshot_channel_st  | 76ns |
| local_once_event_managed    | 38ns |
| pooled_local_once_event_ptr | 27ns |
| pooled_local_once_event_rc  | 31ns |
| pooled_local_once_event_ref | 24ns |

在评估复杂的应用逻辑时,采取整体视角可能至关重要——不仅基准逻辑的运行速度很重要,其使用的能量(处理器时间)也很重要。也许代码还在后台线程上运行逻辑,或者代码只是在系统调用上阻塞了一些线程一段时间,这消耗了挂钟时间却没有消耗处理器时间。在复杂场景中,这些都是我们必须考虑的因素。all_the_time 允许我们跟踪进程消耗的处理器时间,除了挂钟时间之外。 它与 Criterion 集成良好,并得到 par_bench 的原生支持。

Allocation statistics:

| Operation                      | Mean bytes | Mean count |
|--------------------------------|------------|------------|
| futures_oneshot_channel        |        128 |          1 |
| local_once_event_managed       |         48 |          1 |
| pooled_local_once_event_ptr    |          0 |          0 |
| pooled_local_once_event_rc     |          0 |          0 |
| pooled_local_once_event_ref    |          0 |          0 |

内存分配是万恶之源。让典型应用提速最简单且最有效的方法,就是消除其中的内存分配——这往往能将性能提升数倍。在消除之前,我们需要先进行测量。alloc_tracker 让我们能够精确测量特定代码片段分配了多少堆内存。它与 Criterion 集成良好,并得到 par_bench 的原生支持。

一旦我们了解了内存分配的量级,就可以开始做出改变。最简单的方法是修改算法,使其无需内存分配,但有时这并不切实际。尽管如此,全局 Rust 内存分配器(无论使用哪一个)都是一种通用机制,而这种通用性会以性能为代价。如果我们分配了大量特定大小的对象,就可以受益于专用分配器,它们会保留内存以供复用,从而使下一次分配变得简单且快速。

尽管分配器 API 仍然是不稳定的 Rust 特性,但存在稳定 API 的替代方案。 专用分配器的另一个术语是对象池,infinity_pool 提供了多种对象池,从基础的 Vec<T> 风格的固定对象集合到可分配任意类型对象的 类型无关对象池。虽然安全 API 变体与不安全 API 变体相比存在相当大的 开销,但在许多条件下,两者都可以超越使用全局内存分配器的效率。效果可能因情况而异——测量 100 次, 裁剪 10 次。

高性能代码中内存分配的一个意外来源是信号。我们习惯于 认为一次性通道是廉价且高效的东西,虽然这是真的,但它们 仍然建立在从堆分配的共享内存之上。你创建的每个信号通道都是 一次堆分配,它们会迅速累积!events_once 为你提供池化的 信号通道,利用 infinity_pool 来重用内存分配,同时 提供单线程和不安全代码管理的事件,以降低特定 场景下的开销。

bagels_cooked_weight_grams: 2300; sum 744000; mean 323
value <=    0 [    0 ]: 
value <=  100 [    0 ]: 
value <=  200 [ 1300 ]: ∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
value <=  300 [    0 ]: 
value <=  400 [    0 ]: 
value <=  500 [    0 ]: 
value <=  600 [ 1000 ]: ∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
value <=  700 [    0 ]: 
value <=  800 [    0 ]: 
value <=  900 [    0 ]: 
value <= 1000 [    0 ]: 
value <= +inf [    0 ]: 

人们很容易认为,一旦基准测试看起来不错,性能和效率就已经实现了。 然而,时间会让所有人犯傻!现实世界的数据往往表现出令人惊讶的行为——在我们以为 只会生成 500 个任务时,HTTP 栈中一个意外的实现细节可能会导致生成 5 亿个任务!基准测试和信念是不够的。nm 提供了一个非常高性能 且极简的指标框架,适合每秒进行数百万次测量。只有 凭借真实数据,我们才能确保实现了真正的性能。nm_otel 将 nm 指标桥接到 OpenTelemetry,以便导出到任何兼容的后端。

许多对高性能逻辑进行插桩的尝试都是自相矛盾的,因为很少有人预料到 时间本身是缓慢的。测量时间,就是如此!Instant::now() 是一个极其缓慢的 操作——切勿在高性能代码中使用它,因为仅仅捕获时间戳就可能严重 降低性能。精确的计时信息只能针对大批量的 迭代进行测量,以便一个测量跨度覆盖 100 毫秒以上的工作量。对于可以 牺牲精度但仍希望获得令人满意性能的情况,fast_time 提供了 一个查询成本更低的时钟。它不会给你精确的数字,但每秒 查询数万次是安全的。

单次基准测试运行只是一个快照。真正重要的数字随时间推移而显现:一条在数十次提交中缓慢退化的热路径,或一次“无害”的重构使分配数量翻倍。cargo-bench-history 维护着您基准测试结果的长期历史记录——来自 Criterionall_the_timealloc_trackerCallgrind——并对其进行以检测跨提交的回归和改进,从而确保性能变化不会在一次发布与下一次发布之间被忽视。

附加内容

由本项目开发并发布的支持包:

  • awaiter_set - 用于异步同步原语的零分配 awaiter 跟踪。
  • cargo-detect-package - 一个 cargo 子命令,用于根据提供的路径检测所使用的包,并在该包上运行另一个子命令。
  • cargo-freeze-deps - 一个 cargo 子命令,将 Cargo.toml 中浮动的依赖版本固定为其解析后的字面值。
  • cpulist - 用于解析和输出 Linux cpulist 字符串的工具,由 many_cpus 使用。
  • events - 用于多用途信号传递的异步手动重置和自动重置事件。
  • future_deque - 基于池的 deque 集合,用于管理一组 futures,并对轮询顺序和结果获取进行精确控制。
  • new_zealand - 用于处理非零整数的工具

仓库中存在但与一般受众无关的包:

  • benchmarks - 用于探索相关场景并指导 Folo 开发的基准测试集合。
  • cargo-bench-history-faker - 不支持的合成基准输出生成器,用于端到端验证 cargo-bench-history(在本仓库及同级仓库中);作为便捷二进制文件发布,但没有稳定的 API 或 CLI。
  • cargo-bench-history-stress - 按需压力测试框架,用于播种合成基准历史并计时 cargo-bench-history 分析模式;未发布。
  • folo_ffi - 用于处理 FFI 逻辑的工具;存在于 Folo 包中供内部使用;没有稳定的 API 表面。
  • folo_utils - 用于 Folo 包内部使用的工具;存在于 Folo 包中供内部使用;没有稳定的 API 表面。
  • testing - Folo 包中用于测试和示例的私有辅助函数。
  • ui_tests - 工作区包的编译时 UI 测试;未发布。
  • 各种 _impl 包(以及 linked_macros/cbh_* 系列),仅用于在实现层面分离公共和私有 API 表面;请勿直接引用它们。

开发环境设置

参见 DEVELOPMENT.md.

质量保证

本项目旨在达到高质量标准:

全面测试 - 所有包均经过广泛的单元测试、集成测试和 doctest 测试
Miri 验证 - 所有包均通过 Miri 进行的严格 Rust 内存安全验证
变异测试 - 通过 cargo-mutants 进行的全面变异测试验证代码质量
高测试覆盖率 - 通过 cargo-llvm-cov 测量并维护测试覆盖率
墙钟基准测试 - 热路径由 Criterion 基准测试覆盖(通常通过 par_bench 实现多线程),以检测实际性能变化
Callgrind 基准测试 - 选定的热路径还由基于 Valgrind/Callgrind 的一次性基准测试覆盖,以实现确定性的、运行间稳定的指令计数和模拟缓存行为。参见 docs/callgrind-benchmarks.md
零警告策略 - 所有代码必须编译时不产生任何编译器或 Clippy 警告
广泛的 Clippy 规则 - 在整个工作区强制执行 100+ 自定义 Clippy lint 规则
跨平台验证 - 所有代码均在 Windows 和 Linux 平台上进行测试
自动化 CI/CD - 持续集成在每次提交时运行完整的验证套件
API 文档 - 为所有公共 API 提供完整的 API 文档及内联示例
依赖项审计 - 通过 cargo-audit 定期对所有依赖项进行安全审计
语义版本控制合规性 - 通过 cargo-semver-checks 验证 API 变更是否符合语义版本控制