Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Graph in Rust with unsafe

Graph in Rust with unsafe

- Rust でグラフ構造むずかしい…
- 試行錯誤の過程を書きました
- unsafe 使いましたが、unsafe こわい
- メモリープールを実装してみました
- ヤバいところあったら教えて!

4c41dd5087889e60e220abd377b52c4a?s=128

Terada Yuichiro

January 27, 2020
Tweet

Transcript

  1. Rust と Graph と unsafe てらだ @ CADDi

  2. Rust でグラフ構造、難しいですよね… • 試行錯誤したことを語ります • もっと良い方法があれば教えてください >< • unsafe こわい

    ◦ unsafe 使っています ◦ とはいえ unsafe こわい ◦ 深刻な未定義動作を踏んでいないか心配 ◦ 筆者は Rust の未定義動作について詳しくありません ◦ ヤバいところあれば教えてください ◦ 参考にされる方は、自己責任でお願いします
  3. 相互参照したい struct Node<'a, T> { pub value: T, other: Option<&'a

    Self>, } Node Node other other
  4. コンパイルエラー 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 を変更できない!
  5. Cell を使えば解決 struct Node<'a, T> { pub value: T, other:

    Cell<Option<&'a Self>>, } 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<T> Cell<T> { pub fn set(&self, val: T) { … } } mut じゃないのでOK
  6. Node を動的に増やしたい let mut nodes: Vec<Box<Node<usize>>> = 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 ができない
  7. typed_arena で解決 use typed_arena::Arena; let nodes: Arena<Node<usize>> = 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<T> Arena<T> { pub fn alloc(&self, value: T) -> &mut T; } mut じゃないのでOK
  8. Arena が move できない fn construct_graph<'a>() -> Arena<Node<'a, usize>> {

    let arena: Arena<Node<'a, usize>> = Arena::new(); let node = arena.alloc(Node { value: 123, other: Cell::new(None), }); node.other.set(Some(node)); arena } エラー この lifetime のせい
  9. move できないと何が困るか struct Graph<'a, T> { nodes: Arena<Node<'a, T>>, //

    ノード集合 head: &'a Node<'a, T>, // 先頭ノード } fn construct_graph<'a>() -> Graph<'a, usize> { let nodes: Arena<Node<usize>> = 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 のグラフ構造を 内部に持つコレクション型 が作れない
  10. 生ポインタと unsafe で解決 pub struct Node<T> { pub value: T,

    other: *mut Self, } pub struct Graph<T> { nodes: Arena<Node<T>>, head: *mut Node<T>, } lifetime が付かない
  11. Graph の構築 fn construct_graph() -> Graph<usize> { let nodes: Arena<Node<usize>>

    = Arena::new(); let node1 = nodes.alloc(Node { … }); let node2 = nodes.alloc(Node { … }); node2.other = node1 as *mut Node<T>; node1.other = node2 as *mut Node<T>; let head = node1 as *mut Node<T>; Graph { nodes, head } } 普通に move できる
  12. unsafe でポインタを参照にキャスト impl<T> Node<T> { pub fn other(&self) -> &Self

    { unsafe { &*self.other } } } impl<T> Graph<T> { pub fn head(&self) -> &Node<T> { unsafe { &*self.head } } } Graph の実装で dangling pointer でないことを保証できればOK
  13. ただし! &mut Node<T> を返してはいけない! impl<T> Graph<T> { // NOT safe!

    pub fn head_mut(&mut self) -> &mut Node<T> { unsafe { &mut *self.head } } } … let mut graph1 = Graph::new(...); let mut graph2 = Graph::new(...); std::mem::swap(graph1.head_mut(), graph2.head_mut()); // Oops!
  14. &mut Node<T> をラップする pub struct NodeRefMut<'a, T>(&'a mut Node<T>); 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<T> Graph<T> { pub fn head_mut(&mut self) -> NodeRefMut<T> { unsafe { NodeRefMut(&mut *self.head) } } } これで完璧に安全なのかは 自信なし。 漏れがあったら教えてください。
  15. typed_arena には free() 関数がない • alloc() 関数しかない • 一度 alloc()

    したメモリは解放できない • free() 関数を用意しないことによって、dangling pointer が生じないことを保証し ている • その代わり、メモリ使用量は単調増加
  16. free() も備えたアロケータ 作れないんだろうか?

  17. こんなのが作れれば良さそう impl<T> Pool<T> { 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> { … } }
  18. HashMap<*const T, Box<T>>

  19. pub struct Pool<T>(HashMap<*const T, Box<T>>); impl<T> Pool<T> { 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<Box<T>> { 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 使ってない
  20. pub struct Node<T> { value: Option<T>, next: *mut Self, prev:

    *mut Self, } pub struct List<T> { nodes: Pool<Node<T>>, sentinel: Box<Node<T>>, } node 双方向 List を作ってみる sentinel node node node node node head tail next prev prev next
  21. ※ Linked List は使うな、Vec を使え • ほとんどの場合、Vec の方が速いはず ◦ 理論上は、要素の挿入

    /削除の計算量は: ▪ Vec … O(n) ▪ Linked List … O(1) ◦ しかし実際は、Vec の方がキャッシュに乗るので速い • ここで Linked List を作るのは、あくまで例 ◦ これから紹介する Linked List の実装に実用的な価値はない ◦ グラフ構造のシンプルな例として
  22. 空の List 生成 impl<T> List<T> { pub fn new() ->

    Self { let mut sentinel = Box::new(Node::<T> { value: None, next: std::ptr::null_mut(), prev: std::ptr::null_mut(), }); sentinel.next = sentinel.deref_mut() as *mut Node<T>; sentinel.prev = sentinel.deref_mut() as *mut Node<T>; Self { nodes: Pool::new(), sentinel, } } sentinel node next prev
  23. next, prev ポインタは常に有効という前提 impl<T> Node<T> { 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<T> { value: Option<T>, next: *mut Self, prev: *mut Self, }
  24. ノードの取得 impl<T> List<T> { pub fn head(&self) -> &Node<T> {

    self.sentinel.next() } pub fn tail(&self) -> &Node<T> { 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
  25. Node の挿入 unsafe fn insert_unsafe(&mut self, next: *mut Node<T>, value:

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

    Option<&Node<T>> { if let Some(node) = self.nodes.free(node) { let next = node.next as *mut Node<T>; let prev = node.prev as *mut Node<T>; unsafe { (*next).prev = prev; (*prev).next = next; Some(&*next) } } else { None } }
  27. 使い方 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番目の要素を削除
  28. 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
  29. でも、この Pool<T> って効率悪くない? • 実体は HashMap<*const T, Box<T>> • アクセスのたびに

    HashMap の探索コストがかかる • Box<T> なので小さなオブジェクトがたくさんできる • 連続したメモリ領域に乗らないのでキャッシュが効かない可能性 • それ memory pool って言わない
  30. もっと速いやつ作ろう

  31. メモリをブロック単位でまとめて確保 enum Entry<T> { Vacant(Option<NonNull<Self>>), Occupied(T), } pub struct Pool<T>

    { blocks: Vec<Box<[Entry<T>]>>, vacant: Option<NonNull<Entry<T>>>, id: PoolId, } Occupied Vacant Occupied Occupied Vacant blocks[0] vacant ... blocks[1]
  32. ブロックの確保(初期化) const BLOCK_SIZE: usize = 1024; fn new_block() -> (NonNull<Entry<T>>,

    Box<[Entry<T>]>) { 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
  33. alloc() Vacant Vacant Vacant Vacant Vacant None vacant Vacant Vacant

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

    Vacant Vacant Vacant T None vacant
  35. ブロックの解放はしない ptr Vacant Vacant Vacant Vacant T None vacant Vacant

    Vacant Vacant Vacant Vacant None vacant ブロックが全部 Vacant になっても解放せずそのまま保持 → Pool 全体が drop されないかぎり *mut Entry<T> は有効なポインタ
  36. インターフェイス impl<T> Pool<T> { pub fn new() -> Self {

    … } pub fn alloc(&mut self, value: T) -> Ptr<T> { … } pub fn free(&mut self, p: Ptr<T>) -> bool { … } pub fn get(&self, p: Ptr<T>) -> Option<Ref<T>> { … } pub fn get_mut(&mut self, p: Ptr<T>) -> Option<&mut T> { … } } - Ptr<T> … *mut Entry<T> のラッパー - Ref<T> … &Entry<T> のラッパー
  37. Ptr<T> が必要な理由 let mut pool1 = Pool::new(); let mut pool2

    = Pool::new(); let ptr = pool1.alloc(); pool2.free(ptr); // Oops! pub struct Pool<T> { blocks: Vec<Box<[Entry<T>]>>, vacant: Option<NonNull<Entry<T>>>, id: PoolId, } pub struct Ptr<T> { ptr: NonNull<Entry<T>>, pool_id: PoolId, } impl<T> Pool<T> { pub fn free(&mut self, p: Ptr<T>) -> bool { assert!(h.pool_id == self.id()); … } } 所属する Pool を識別するため、 Pool に一意な ID を与える
  38. Ref<’a, T> も同様 pub struct Ref<'a, T> { value: &'a

    T, entry: &'a Entry<T>, pool_id: PoolId, } impl<'a, T> Deref for Ref<'a, T> { … } // value の取得 impl<'a, T> From<Ref<'a, T>> for Ptr<T> { … } // Ptr<T> への変換
  39. Ptr<T> から Ref<’a, T> の取得 impl<T> Ptr<T> { pub unsafe

    fn as_ref<'a>(&self) -> Option<Ref<'a, T>> { let entry = &*self.ptr.as_ptr(); // unsafe な操作 match entry { Entry::Occupied(value) => Some(Ref { value, entry, pool_id: self.pool_id, }), _ => None, } } impl<T> Pool<T> { pub fn get(&self, p: Ptr<T>) -> Option<Ref<T>> { assert!(p.pool_id == self.id()); unsafe { p.as_ref() } } } HashMap の探索コストなし
  40. ベンチマーク • List<usize> に N 個の要素を格納 • 下記の操作を N 回ランダムに繰り返す

    ◦ 指している要素の移動 ◦ 指している要素の削除 ◦ 指している要素の位置に新しい値を挿入 • N = 1,000,000 (100万)
  41. ベンチマーク結果 • 368.0047 ms … HashMap<*const T, Box<T>> による Pool<T>

    • 37.4862 ms … 頑張って作った方の Pool<T> • 50.3582 ms … typed_arena によるもの • 10倍だぞ10倍 • typed_arena より速いとは思わなかった ◦ たぶん、一度確保したメモリ領域を再利用できているから
  42. ソースコード • https://github.com/u1roh/mepoo … Pool<T> • https://github.com/u1roh/graph_in_rust_with_unsafe … 発表用に書いたやつ 利用は自己責任で!