Ferroc v1.0.0-pre.1 现已在crates.io上发布。期待大家的使用和反馈来持续改进,最终完备地发布第一个正式版。
主要的功能和实现改动
- 减小了
allocate_*
系列函数接口的Result
大小,与之相应的具体Error
类型将不能够再直接获得(变为core::alloc::AllocError
),转而可以通过开启新增的error-log
功能来记录错误日志; - TLS变量转而使用专门的缓存结构体,可以在新旧线程交替时复用,减小TLS对应内存的重复分配;
- 各种代码生成上的优化,比如尾调用、冷热路径分离等等;
- 暂时移除了数据统计和大于10MB对齐的内存块分配的支持,会在未来的版本中重新加入;
- 增加了C/C++的使用支持。
TLS缓存介绍
ferroc::heap::ThreadLocal
是高度灵活的TLS缓存,用户可以选择手动或自动管理其中缓存的生命周期。其内部使用了指数增长的内存分配策略,并直接通过BaseAlloc
分配空间给线程本地的Context
和Heap
。
自动管理缓存生命周期
通过ferroc::heap::ThreadData
可以直接从ThreadLocal
获得一块自动管理生命周期的线程本地缓存。该缓存可以被分配在栈上(以作为临时的上下文),或者存储在实际的TLS变量中。
栈上使用示例:
#![feature(allocator_api)]
use core::pin::pin;
use ferroc::{
arena::Arenas,
base::Mmap,
heap::{ThreadLocal, ThreadData}
};
let arenas = Arenas::new(Mmap);
let thread_local = pin!(ThreadLocal::new(&arenas));
let thread_data = ThreadData::new(thread_local.as_ref());
let mut vec = Vec::with_capacity_in(5, &thread_data);
vec.extend([1, 2, 3, 4, 5]);
assert_eq!(vec.iter().sum::<i32>(), 15);
存储于TLS的使用示例:
static ARENAS: Arenas<Mmap> = Arenas::new(Mmap);
static THREAD_LOCAL: ThreadLocal<Mmap> = ThreadLocal::new(&ARENAS);
thread_local! {
static THREAD_DATA: ThreadData<'static, 'static, Mmap>
= ThreadData::new(Pin::static_ref(&THREAD_LOCAL));
}
THREAD_DATA.with(|td| {
let mut vec = Vec::with_capacity_in(5, td);
vec.extend([1, 2, 3, 4, 5]);
assert_eq!(vec.iter().sum::<i32>(), 15);
})
然而,由于Rust本身TLS变量的存储和销毁并用到了堆内存,因此直接将THREAD_DATA
包装成单元结构体来做全局分配器将会造成递归爆栈。这时候,自动管理生命周期的方式已经不再有效。
手动管理生命周期则依靠ThreadLocal
直接提供的成员方法assign
、get
和put
。其中后二者为unsafe
。以下给出两种手动管理的示例,可以根据需要选用(示例中register_thread_dtor
是手动注册TLS析构器的函数,提供者有pthread,可查阅相关文档):
- 存储线程唯一的缓存ID:
static ARENAS: Arenas<Mmap> = Arenas::new(Mmap);
static THREAD_LOCAL: ThreadLocal<Mmap> = ThreadLocal::new(&ARENAS);
#[thread_local]
static ID: Cell<Option<NonZeroU64>> = Cell::new(None);
fn with_heap<T>(f: impl FnOnce(&Heap) -> T) -> T {
f(Pin::get_ref(match ID.get() {
Some(id) => unsafe { Pin::static_ref(&THREAD_LOCALS).get(id) },
None => {
let (heap, id) = Pin::static_ref(&THREAD_LOCALS).assign();
ID.set(id);
unsafe { register_thread_dtor(id, || Pin::static_ref(&THREAD_LOCALS).put(id)) };
heap
}
}))
}
- 直接存储线程本地堆的引用:
#[thread_local]
static HEAP: Cell<Pin<&Heap>> = Cell::new(THREAD_LOCALS.empty_heap());
fn with_heap<T>(f: impl FnOnce(&Heap) -> T) -> T {
f(&HEAP.get())
}
fn with_heap_fallback<T, F>(f: F) -> T
where
F: for<'a> FnOnce(&'a Heap<'static, 'static>, fn() -> &'a Heap<'static, 'static>) -> T,
{
fn fallback<'a>() -> &'a Heap<'static, 'static> {
let (heap, id) = Pin::static_ref(&THREAD_LOCALS).assign();
HEAP.set(heap);
unsafe { register_thread_dtor(id, || Pin::static_ref(&THREAD_LOCALS).put(id)) };
Pin::get_ref(heap)
}
f(Pin::get_ref(HEAP.get()), fallback)
}
其中,前者的优势是简单;而后者的优势则是优化代码路径——deallocate
不需要当前的Heap
初始化,而allocate
可能才需要。后者的fallback可以作为一个参数传入新加入的Heap::allocate_with
成员函数用来提供当前堆未初始化时的替代,用来优化代码路径。
最后,当然还是直接使用ferroc::config!
,或者直接使用默认的ferroc::Ferroc
最为方便:
// 内部默认使用了`ferroc::heap::ThreadLocal`。
ferroc::config!(pub Custom => ferroc::base::Mmap; pthread);
C/C++用户使用示例
- 下载Rust工具链:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs |\
sh -- -y --toolchain nightly --profile minimal -c rust-src
cmake
然后make
来自动化生成:
mkdir target && cd target # 注意不要改成build等其他目录
cmake .. && make
生成的文件有(相对路径):target/libferroc.so
、target/libferroc.a
和ferroc.h
- 安装动态库、静态库和头文件到本地:
sudo make install
CMake的通用配置选项比如-DCMAKE_BUILD_TYPE
、--install-prefix
等均支持。
并且有其他的自定义选项(cmake -D
):
FE_TRACK_VALGRIND
: 使用valgrind追踪内存UB;FE_FINER_GRAINED
: 提升内存效率,但是可能会导致使用SIMD的程序发生内存对齐错误;FE_PGO_GATHER
: 启用 PGO (Profile-Guided Optimization) 的数据收集(至项目目录下target/pgo
)。FE_PGO_USE
: 使用PGO的数据优化生成目标(需要与Rust工具链版本匹配的llvm-profdata
,可在第一步安装Rust工具链时在命令尾部增加llvm-tools
来顺带安装)。
基准测试结果更新
时间消耗:
内存消耗:
欢迎大家试用并提供反馈!
Ext Link: https://github.com/js2xxx/ferroc/tree/v1.0.0-pre.1
评论区
写评论还没有评论