Linux 所使用的 slab 分配器的基礎是 Jeff Bonwick 爲 SunOS 操做系統首次引入的一種算法。Jeff 的分配器是圍繞對象緩存進行的。在內核中,會爲有限的對象集(例如文件描述符和其餘常見結構)分配大量內存。Jeff 發現對內核中普通對象進行初始化所需的時間超過了對其進行分配和釋放所需的時間。所以他的結論是不該該將內存釋放回一個全局的內存池,而是將內存保持爲針對特定目而初始化的狀態。git
咱們來看看rust中如何實現這一算法,也藉此學習rust的應用技術。github
源碼: carllerche/slab:Preallocate memory for values of a given type.算法
先來看一個基礎數據結構Slot,這個數據結構對後面理解Slab相當重要。數組
enum Slot<T> { Empty(usize), Filled(T), Invalid, }
能夠把Slot想象爲一個抽屜,有三種狀態:緩存
一個Slot能夠爲空,能夠無效,能夠裝一個T類型對象且只能裝一個。數據結構
接着來看Slab數據結構,這個數據結構表明着從內存中申請的一大塊內存,這一大塊內存中能夠存儲許多相同類型的對象,就像一個包含許多抽屜的大櫃子。函數
/// A preallocated chunk of memory for storing objects of the same type. pub struct Slab<T, I = usize> { // Chunk of memory entries: Vec<Slot<T>>, // Number of Filled elements currently in the slab len: usize, // Offset of the next available slot in the slab. Set to the slab's // capacity when the slab is full. next: usize, _marker: PhantomData<I>, }
Slab就至關於一長排抽屜。包含四個成員:學習
Slab有兩個類型參數:操作系統
經過這兩個數據結構Slab內存分配模型就創建完畢了,Slab分配的核心思想是「用完不還」。一次性獲取一塊大的內存,之後不夠了還能夠再申請內存,可是申請了我就不還,用完了我也暫時不還,省得下次要用的時候還要申請(主要解決的問題是在頻繁的使用過程當中,申請抽屜的時間大於使用抽屜的時間)。code
那使用Slab的方式就是,我預計一下今天我要裝20個東西(T),那我就一次性申請一個包含20個抽屜的Slab(大櫃子),來一個東西我用一個抽屜,用完了我櫃子仍是先留着,等我不用了,再把櫃子還回去。
按使用順序,首先來看申請Slab的方法:
impl<T, I> Slab<T, I> { /// Returns an empty `Slab` with the requested capacity pub fn with_capacity(capacity: usize) -> Slab<T, I> { let entries = (1..capacity + 1) .map(Slot::Empty) .collect::<Vec<_>>(); Slab { entries: entries, next: 0, len: 0, _marker: PhantomData, } } ...... }
根據你要的大小,返回一個大櫃子,其中的每一個抽屜都是空的,什麼也沒有裝,利用Vec來申請內存。
這個方法自己沒有什麼難懂的,只是有一個語法細節須要特別注意:
let entries = (1..capacity + 1) .map(Slot::Empty) .collect::<Vec<_>>();
這一行在建立空抽屜,(1..capacity + 1)
表示建立一個Range,每一個元素爲usize,從1到capacity+1。接下來經過map方法建立抽屜,不過這裏傳給map的參數看起來怪怪的,map的定義以下:
fn map<B, F>(self, f: F) -> Map<Self, F> where F: FnMut(Self::Item) -> B
傳給map的參數應該是一個將usize映射爲Slot的函數纔對,而這裏直接傳入的是Slot::Empty,是一個枚舉類型的變元!
再來看看Slot的定義:
enum Slot<T> { Empty(usize), Filled(T), Invalid, }
Empty是一個tupe struct variant。在Rust中申明這樣的變元時,編譯會自動爲其生成一個構造函數
fn Slot::Empty(u: usize) -> Slot { Slot::Empty(u) }
因此這裏將Slot::Empty直接傳遞給map是合法的。
整個建立函數的意思是申請一個包含指定數量的抽屜的大櫃子,爲每一個抽屜編一個號(1..n),且初始狀態爲空。
大櫃子申請好了,如今來看看如何往其中放東西
impl<T, I: Into<usize> + From<usize>> Slab<T, I> { ...... /// Insert a value into the slab, returning the associated token pub fn insert(&mut self, val: T) -> Result<I, T> { match self.vacant_entry() { Some(entry) => Ok(entry.insert(val).index()), None => Err(val), } } ...... /// Returns a handle to a vacant entry. /// /// This allows optionally inserting a value that is constructed with the /// index. pub fn vacant_entry(&mut self) -> Option<VacantEntry<T, I>> { let idx = self.next; if idx >= self.entries.len() { return None; } Some(VacantEntry { slab: self, idx: idx, }) } ......
先來看這一句
I: Into<usize> + From<usize>
這一句要求索引類型I是能夠與usize進行來回轉換的。以前咱們看到每一個抽屜都有一個usize的編號,而I又是用來索引抽屜的,若是I能夠與uszie進行映射,那麼經過I找抽屜的任務就能夠完成。
爲何不直接用usize來作索引,還要那麼麻煩的接受一個任意類型呢?若是直接使用usize作索引,那麼一個給定的索引編號就直接對應到一個抽屜,索引和抽屜之間是一一映射的關係。若是使用I作索引,多個I能夠映射到同一個usize就能夠作到索引和抽屜之間的多對一關係,帶來更多的靈活性,並且能表達更多信息,mio庫的做者就使用業務含意豐富的Token索引替換了默認的簡單的usize索引。
再來看插入函數
pub fn insert(&mut self, val: T) -> Result<I, T> { ... }
從函數申明看出,調用這個方法會修改Slab內部數據,傳入要存儲的對象val,若是存儲成功,返回索引,不成功返回傳入的val。這就比如我請你幫我把東西寄存在抽屜裏,若是有合適的抽屜,你幫我把東西放好後告訴我放在第幾個抽屜裏了,若是沒有找到合適的抽屜,你把東西還給我。
第一步就是找空箱子,經過vacant_entry方法完成
pub fn vacant_entry(&mut self) -> Option<VacantEntry<T, I>> { ... }
方法申明中看出,若是找到返回一個VacantEntry對象,沒有返回None。
pub struct VacantEntry<'a, T: 'a, I: 'a> { slab: &'a mut Slab<T, I>, idx: usize, }
slab爲大櫃子的可變引用,idx是找到的抽屜的編號。 根據vacant_entry方法的實現能夠知道找箱子的算法。下一個可用的抽屜編號保存在next中,只要不越界,就直接返回。
未完待續……