Slide 1

Slide 1 text

Rust と Graph と unsafe てらだ @ CADDi

Slide 2

Slide 2 text

Rust でグラフ構造、難しいですよね… ● 試行錯誤したことを語ります ● もっと良い方法があれば教えてください >< ● unsafe こわい ○ unsafe 使っています ○ とはいえ unsafe こわい ○ 深刻な未定義動作を踏んでいないか心配 ○ 筆者は Rust の未定義動作について詳しくありません ○ ヤバいところあれば教えてください ○ 参考にされる方は、自己責任でお願いします

Slide 3

Slide 3 text

相互参照したい struct Node<'a, T> { pub value: T, other: Option<&'a Self>, } Node Node other other

Slide 4

Slide 4 text

コンパイルエラー let mut node1 = Box::new(Node { value: 123, other: None, // None で初期化 }); let mut node2 = Box::new(Node { value: 456, other: None, // None で初期化 }); // ここで相互参照を作る node1.other = Some(&node2); node2.other = Some(&node1); // compilation error! node1.other = Some(&node2); ------ borrow of `node2.other` occurs here node2.other = Some(&node1); ^^^^^^^^^^^^^^^^^^^^^^^^^^ | assignment to borrowed `node2.other` occurs here borrow later used here node2 が borrow されているので… ここで node2 を変更できない!

Slide 5

Slide 5 text

Cell を使えば解決 struct Node<'a, T> { pub value: T, other: Cell>, } let node1 = Box::new(Node { value: 123, other: Cell::new(None), // None で初期化 }); let node2 = Box::new(Node { value: 456, other: Cell::new(None), // None で初期化 }); // ここで相互参照を作る node1.other.set(Some(&node2)); node2.other.set(Some(&node1)); impl Cell { pub fn set(&self, val: T) { … } } mut じゃないのでOK

Slide 6

Slide 6 text

Node を動的に増やしたい let mut nodes: Vec>> = Vec::new(); nodes.push(Box::new(Node { … })); nodes.push(Box::new(Node { … })); nodes[1].other.set(Some(&nodes[0])); nodes[0].other.set(Some(&nodes[1])); // ここまではOK nodes.push(Box::new(Node { … })); // ここでエラー! nodes が borrow されているので、 ここで mutable な borrow ができない

Slide 7

Slide 7 text

typed_arena で解決 use typed_arena::Arena; let nodes: Arena> = Arena::new(); let node1 = nodes.alloc(Node { … }); let node2 = nodes.alloc(Node { … }); node2.other.set(Some(node1)); node1.other.set(Some(node2)); let node3 = nodes.alloc(Node { … }); node3.other.set(Some(node1)); impl Arena { pub fn alloc(&self, value: T) -> &mut T; } mut じゃないのでOK

Slide 8

Slide 8 text

Arena が move できない fn construct_graph<'a>() -> Arena> { let arena: Arena> = Arena::new(); let node = arena.alloc(Node { value: 123, other: Cell::new(None), }); node.other.set(Some(node)); arena } エラー この lifetime のせい

Slide 9

Slide 9 text

move できないと何が困るか struct Graph<'a, T> { nodes: Arena>, // ノード集合 head: &'a Node<'a, T>, // 先頭ノード } fn construct_graph<'a>() -> Graph<'a, usize> { let nodes: Arena> = Arena::new(); let node1 = nodes.alloc(Node { … }); let node2 = nodes.alloc(Node { … }); node2.other.set(Some(node1)); node1.other.set(Some(node2)); Graph { nodes, head: node1 } // ERROR! } ここに lifetime が付いてしまう Node のグラフ構造を 内部に持つコレクション型 が作れない

Slide 10

Slide 10 text

生ポインタと unsafe で解決 pub struct Node { pub value: T, other: *mut Self, } pub struct Graph { nodes: Arena>, head: *mut Node, } lifetime が付かない

Slide 11

Slide 11 text

Graph の構築 fn construct_graph() -> Graph { let nodes: Arena> = Arena::new(); let node1 = nodes.alloc(Node { … }); let node2 = nodes.alloc(Node { … }); node2.other = node1 as *mut Node; node1.other = node2 as *mut Node; let head = node1 as *mut Node; Graph { nodes, head } } 普通に move できる

Slide 12

Slide 12 text

unsafe でポインタを参照にキャスト impl Node { pub fn other(&self) -> &Self { unsafe { &*self.other } } } impl Graph { pub fn head(&self) -> &Node { unsafe { &*self.head } } } Graph の実装で dangling pointer でないことを保証できればOK

Slide 13

Slide 13 text

ただし! &mut Node を返してはいけない! impl Graph { // NOT safe! pub fn head_mut(&mut self) -> &mut Node { unsafe { &mut *self.head } } } … let mut graph1 = Graph::new(...); let mut graph2 = Graph::new(...); std::mem::swap(graph1.head_mut(), graph2.head_mut()); // Oops!

Slide 14

Slide 14 text

&mut Node をラップする pub struct NodeRefMut<'a, T>(&'a mut Node); impl<'a, T> NodeRefMut<'a, T> { pub fn value(&self) -> &T { &self.0.value } pub fn value_mut(&mut self) -> &mut T { &mut self.0.value } pub fn other(&mut self) -> Self { unsafe { Self(std::mem::transmute(self.0.other)) } } } impl Graph { pub fn head_mut(&mut self) -> NodeRefMut { unsafe { NodeRefMut(&mut *self.head) } } } これで完璧に安全なのかは 自信なし。 漏れがあったら教えてください。

Slide 15

Slide 15 text

typed_arena には free() 関数がない ● alloc() 関数しかない ● 一度 alloc() したメモリは解放できない ● free() 関数を用意しないことによって、dangling pointer が生じないことを保証し ている ● その代わり、メモリ使用量は単調増加

Slide 16

Slide 16 text

free() も備えたアロケータ 作れないんだろうか?

Slide 17

Slide 17 text

こんなのが作れれば良さそう impl Pool { pub fn alloc(&mut self, x: T) -> &mut T { … } // free がある pub fn free(&mut self, ptr: *const T) { … } // ptr が dangling のときは None を返す pub fn get(&self, ptr: *const T) -> Option<&T> { … } pub fn get_mut(&mut self, ptr: *const T) -> Option<&mut T> { … } }

Slide 18

Slide 18 text

HashMap<*const T, Box>

Slide 19

Slide 19 text

pub struct Pool(HashMap<*const T, Box>); impl Pool { pub fn new() -> Self { Self(HashMap::new()) } pub fn alloc(&mut self, x: T) -> &mut T { let obj = Box::new(x); self.0.entry(obj.deref() as *const T).or_insert(obj) } pub fn free(&mut self, ptr: *const T) -> Option> { self.0.remove(&ptr) } pub fn get(&self, ptr: *const T) -> Option<&T> { self.0.get(&ptr).map(Deref::deref) } pub fn get_mut(&mut self, ptr: *const T) -> Option<&mut T> { self.0.get_mut(&ptr).map(DerefMut::deref_mut) } } ここでは unsafe 使ってない

Slide 20

Slide 20 text

pub struct Node { value: Option, next: *mut Self, prev: *mut Self, } pub struct List { nodes: Pool>, sentinel: Box>, } node 双方向 List を作ってみる sentinel node node node node node head tail next prev prev next

Slide 21

Slide 21 text

※ Linked List は使うな、Vec を使え ● ほとんどの場合、Vec の方が速いはず ○ 理論上は、要素の挿入 /削除の計算量は: ■ Vec … O(n) ■ Linked List … O(1) ○ しかし実際は、Vec の方がキャッシュに乗るので速い ● ここで Linked List を作るのは、あくまで例 ○ これから紹介する Linked List の実装に実用的な価値はない ○ グラフ構造のシンプルな例として

Slide 22

Slide 22 text

空の List 生成 impl List { pub fn new() -> Self { let mut sentinel = Box::new(Node:: { value: None, next: std::ptr::null_mut(), prev: std::ptr::null_mut(), }); sentinel.next = sentinel.deref_mut() as *mut Node; sentinel.prev = sentinel.deref_mut() as *mut Node; Self { nodes: Pool::new(), sentinel, } } sentinel node next prev

Slide 23

Slide 23 text

next, prev ポインタは常に有効という前提 impl Node { pub fn is_sentinel(&self) -> bool { self.value.is_none() } pub fn next(&self) -> &Self { unsafe { &*self.next } } pub fn prev(&self) -> &Self { unsafe { &*self.prev } } pub fn value(&self) -> &T { assert!(!self.is_sentinel()); self.value.as_ref().unwrap() } } pub struct Node { value: Option, next: *mut Self, prev: *mut Self, }

Slide 24

Slide 24 text

ノードの取得 impl List { pub fn head(&self) -> &Node { self.sentinel.next() } pub fn tail(&self) -> &Node { self.sentinel.prev() } pub fn is_empty(&self) -> bool { self.head().is_sentinel() } } node sentinel node node node node node head tail next prev prev next

Slide 25

Slide 25 text

Node の挿入 unsafe fn insert_unsafe(&mut self, next: *mut Node, value: T) { let prev: *mut Node = (*next).prev; let node: *mut Node = self.nodes.alloc(Node { value: Some(value), next, prev, }) as *mut Node; (*next).prev = node; (*prev).next = node; } pub fn insert(&mut self, pos: *const Node, value: T) -> bool { if let Some(next) = self.nodes.get_mut(pos) { let next = next as *mut Node; unsafe { self.insert_unsafe(next, value) } true } else { false } }

Slide 26

Slide 26 text

Node の削除 pub fn remove(&mut self, node: *const Node) -> Option<&Node> { if let Some(node) = self.nodes.free(node) { let next = node.next as *mut Node; let prev = node.prev as *mut Node; unsafe { (*next).prev = prev; (*prev).next = next; Some(&*next) } } else { None } }

Slide 27

Slide 27 text

使い方 let mut list = List::new(); list.insert(list.head(), 1); // 先頭に 1 を挿入 list.insert(list.sentinel(), 2); // 末尾に 2 を挿入 assert_eq!(*list.head().value(), 1); // 先頭の値を取得 assert_eq!(*list.head().next().value(), 2); // 2番目の値を取得 list.remove(list.head().next()); // 2番目の要素を削除

Slide 28

Slide 28 text

dangling な参照は作れない let head = list.head(); // 先頭ノードを取得 list.remove(list.head()); // 先頭ノードを削除 println!("{}", head.value()); // dangling pointer を参照!? error[E0502]: cannot borrow `list` as mutable because it is also borrowed as immutable | 220 | let head = list.head(); | ---- immutable borrow occurs here 221 | list.remove(list.head()); | ^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here 222 | println!("{}", head.value()); | ---- immutable borrow later used here

Slide 29

Slide 29 text

でも、この Pool って効率悪くない? ● 実体は HashMap<*const T, Box> ● アクセスのたびに HashMap の探索コストがかかる ● Box なので小さなオブジェクトがたくさんできる ● 連続したメモリ領域に乗らないのでキャッシュが効かない可能性 ● それ memory pool って言わない

Slide 30

Slide 30 text

もっと速いやつ作ろう

Slide 31

Slide 31 text

メモリをブロック単位でまとめて確保 enum Entry { Vacant(Option>), Occupied(T), } pub struct Pool { blocks: Vec]>>, vacant: Option>>, id: PoolId, } Occupied Vacant Occupied Occupied Vacant blocks[0] vacant ... blocks[1]

Slide 32

Slide 32 text

ブロックの確保(初期化) const BLOCK_SIZE: usize = 1024; fn new_block() -> (NonNull>, Box<[Entry]>) { let mut block = Vec::with_capacity(Self::BLOCK_SIZE); let mut vacant = None; for _ in 0..Self::BLOCK_SIZE { block.push(Entry::Vacant(vacant)); vacant = NonNull::new(block.last_mut().unwrap() as *mut _); } (vacant.unwrap(), block.into_boxed_slice()) } Vacant Vacant Vacant Vacant Vacant None vacant

Slide 33

Slide 33 text

alloc() Vacant Vacant Vacant Vacant Vacant None vacant Vacant Vacant Vacant T Vacant None vacant Vacant Vacant Vacant T T None vacant

Slide 34

Slide 34 text

free(ptr) Vacant Vacant Vacant T T None vacant ptr Vacant Vacant Vacant Vacant T None vacant

Slide 35

Slide 35 text

ブロックの解放はしない ptr Vacant Vacant Vacant Vacant T None vacant Vacant Vacant Vacant Vacant Vacant None vacant ブロックが全部 Vacant になっても解放せずそのまま保持 → Pool 全体が drop されないかぎり *mut Entry は有効なポインタ

Slide 36

Slide 36 text

インターフェイス impl Pool { pub fn new() -> Self { … } pub fn alloc(&mut self, value: T) -> Ptr { … } pub fn free(&mut self, p: Ptr) -> bool { … } pub fn get(&self, p: Ptr) -> Option> { … } pub fn get_mut(&mut self, p: Ptr) -> Option<&mut T> { … } } - Ptr … *mut Entry のラッパー - Ref … &Entry のラッパー

Slide 37

Slide 37 text

Ptr が必要な理由 let mut pool1 = Pool::new(); let mut pool2 = Pool::new(); let ptr = pool1.alloc(); pool2.free(ptr); // Oops! pub struct Pool { blocks: Vec]>>, vacant: Option>>, id: PoolId, } pub struct Ptr { ptr: NonNull>, pool_id: PoolId, } impl Pool { pub fn free(&mut self, p: Ptr) -> bool { assert!(h.pool_id == self.id()); … } } 所属する Pool を識別するため、 Pool に一意な ID を与える

Slide 38

Slide 38 text

Ref<’a, T> も同様 pub struct Ref<'a, T> { value: &'a T, entry: &'a Entry, pool_id: PoolId, } impl<'a, T> Deref for Ref<'a, T> { … } // value の取得 impl<'a, T> From> for Ptr { … } // Ptr への変換

Slide 39

Slide 39 text

Ptr から Ref<’a, T> の取得 impl Ptr { pub unsafe fn as_ref<'a>(&self) -> Option> { let entry = &*self.ptr.as_ptr(); // unsafe な操作 match entry { Entry::Occupied(value) => Some(Ref { value, entry, pool_id: self.pool_id, }), _ => None, } } impl Pool { pub fn get(&self, p: Ptr) -> Option> { assert!(p.pool_id == self.id()); unsafe { p.as_ref() } } } HashMap の探索コストなし

Slide 40

Slide 40 text

ベンチマーク ● List に N 個の要素を格納 ● 下記の操作を N 回ランダムに繰り返す ○ 指している要素の移動 ○ 指している要素の削除 ○ 指している要素の位置に新しい値を挿入 ● N = 1,000,000 (100万)

Slide 41

Slide 41 text

ベンチマーク結果 ● 368.0047 ms … HashMap<*const T, Box> による Pool ● 37.4862 ms … 頑張って作った方の Pool ● 50.3582 ms … typed_arena によるもの ● 10倍だぞ10倍 ● typed_arena より速いとは思わなかった ○ たぶん、一度確保したメモリ領域を再利用できているから

Slide 42

Slide 42 text

ソースコード ● https://github.com/u1roh/mepoo … Pool ● https://github.com/u1roh/graph_in_rust_with_unsafe … 発表用に書いたやつ 利用は自己責任で!