2 个版本
0.1.1 | 2022年10月28日 |
---|---|
0.1.0 | 2022年9月7日 |
#2471 在 Rust 模式
在 2 个crate中使用 (通过 fenris)
13KB
83 行
davenport
davenport
是一个 Rust 微库,提供对中间数据直观的线程局部工作区。
use davenport::{define_thread_local_workspace, with_thread_local_workspace};
#[derive(Default)]
pub struct MyWorkspace {
index_buffer: Vec<usize>
}
define_thread_local_workspace!(WORKSPACE);
fn median_floor(indices: &[usize]) -> Option<usize> {
with_thread_local_workspace(&WORKSPACE, |workspace: &mut MyWorkspace| {
// Re-use buffer from previous call to this function
let buffer = &mut workspace.index_buffer;
buffer.clear();
buffer.copy_from_slice(&indices);
buffer.sort_unstable();
buffer.get(indices.len() / 2).copied()
})
}
查看文档以获取对库的深入了解。
许可证
根据您的选择,受 MIT 和 Apache 2.0 许可证条款的约束。有关详细许可证文本,请参阅 LICENSE-MIT
和 LICENSE-APACHE
。
lib.rs
:
适用于中间数据的直观线程局部工作区。
davenport
是一个具有简单 API 的微库,用于处理线程局部数据,例如中间数据的缓冲区。以下是一个 davenport
API 的简要示例
use davenport::{define_thread_local_workspace, with_thread_local_workspace};
#[derive(Default)]
pub struct MyWorkspace {
index_buffer: Vec<usize>
}
define_thread_local_workspace!(WORKSPACE);
fn median_floor(indices: &[usize]) -> Option<usize> {
with_thread_local_workspace(&WORKSPACE, |workspace: &mut MyWorkspace| {
// Re-use buffer from previous call to this function
let buffer = &mut workspace.index_buffer;
buffer.clear();
buffer.copy_from_slice(&indices);
buffer.sort_unstable();
buffer.get(indices.len() / 2).copied()
})
}
应谨慎使用线程局部存储。在上面的示例中,如果 indices
很大,则可能会分配一个大的缓冲区,并且在程序运行期间不会释放。由于使用线程局部存储的独立函数很少拥有足够的信息来判断缓冲区是否应该保持活动状态,这可能导致不必要的和冗余的内存使用。
在求助于线程局部数据之前,尽量寻找其他解决方案!
动机示例
假设我们必须计算由 Producer
以可变大小的 "块" 产生的元素序列的总和。对于固定元素类型 u32
,我们的代码可能看起来像这样
pub trait Producer {
fn num_elements(&self) -> usize;
fn populate_buffer(&self, buffer: &mut [u32]);
}
fn compute_sum(producer: &dyn Producer) -> u32 {
let mut buffer = vec![u32::MAX; producer.num_elements()];
producer.populate_buffer(&mut buffer);
buffer.iter().sum()
}
如果我们反复调用这个方法,为了避免不断重新分配向量,可能是一个明智的做法。理想情况下,我们能够在函数参数中存储一些持久缓冲区,或者让compute_sum
成为一个具有内部缓冲区的对象的成员方法。然而,有时我们并没有这样的便利,可能是因为我们被限制在不能传递缓冲区的一个现有API中。在这种情况下,一个替代方案可能是将缓冲区存储在线程本地存储中。使用线程本地存储,上述compute_sum
函数可能看起来像这样
fn compute_sum(producer: &dyn Producer) -> u32 {
thread_local! { static BUFFER: std::cell::RefCell<Vec<u32>> = Default::default(); }
BUFFER.with(|rc| {
let mut buffer = rc.borrow_mut();
producer.populate_buffer(&mut *buffer);
buffer.iter().sum()
})
}
现在,让我们想象一下,我们希望我们的函数能够与一组更通用的类型一起工作,而不仅仅是u32
。我们泛化了Producer
特质,但很快意识到我们无法以相同的方式创建一个thread_local!
缓冲区。
use std::ops::{Add, AddAssign};
pub trait Producer<T> {
fn num_elements(&self) -> usize;
fn populate_buffer(&self, buffer: &mut [T]);
}
fn compute_sum<T>(producer: &dyn Producer<T>) -> T
where
T: 'static + Default + Copy + std::iter::Sum
{
// Does not compile!
// error[E0401]: can't use generic parameters from outer function
thread_local! { static BUFFER: std::cell::RefCell<Vec<T>> = Default::default(); }
BUFFER.with(|rc| {
let mut buffer = rc.borrow_mut();
buffer.resize(producer.num_elements(), T::default());
producer.populate_buffer(&mut *buffer);
buffer.iter()
.copied()
.sum()
})
}
实际上,构造一个在类型上泛型的线程本地工作空间通常很困难。然而,我们可以使用davenport
来实现这一点。
use davenport::{define_thread_local_workspace, with_thread_local_workspace};
use std::ops::{Add, AddAssign};
#
fn compute_sum<T>(producer: &dyn Producer<T>) -> T
where
T: 'static + Default + Copy + std::iter::Sum
{
define_thread_local_workspace!(WORKSPACE);
with_thread_local_workspace(&WORKSPACE, |buffer: &mut Vec<T>| {
buffer.resize(producer.num_elements(), T::default());
producer.populate_buffer(buffer);
buffer.iter()
.copied()
.sum()
})
}
davenport
绕过了上述限制,因为实际的线程本地变量是一个Workspace
的实例,这是一个类型擦除工作空间的容器。因此,在上面的例子中真正发生的是,我们构造了一个线程本地Workspace
类型,并请求一个对Vec<T>
的可变引用。如果缓冲区尚未存在,它将默认构造。否则,我们将获得一个之前使用的实例。
限制
目前,尝试递归地使用with_thread_local_workspace
访问相同的 workspace 变量(如上例中的WORKSPACE
),将会引发 panic,因为它依赖于通过RefCell
进行可变借用。虽然技术上可以通过增加davenport
的复杂性来解除这个限制,但在实际使用足够局部的工作空间时,这种情况很少发生,相比之下,在跨整个crate共享单个 workspace 变量时则更为常见。