Help us understand the problem. What is going on with this article?

Rust Memory Container Cheat-sheet

rust-memory-container-cs-3840x2160-dark-back.png

文字で読み書きするとやや大変です。しばらく間を開けてRustを触ったりするとスコシ混乱するかもしれません。と、いうわけでRustでメモリーコンテナー系に触れるモードになった時用に1枚絵のチートシートを整理しました。

文字を書けるQiitaにポストするのでちょっとだけチートシートの解説も乗せます。

おまけ解説

Threads; スレッド群

(rev.0 -> rev.1 でこのチートシート上で最初に現れる選択肢に切り替わりました ⇔ Ownership; 所有権)

Rustではマルチスレッディング実行の安全性を向上するため、 適当に確保したメモリーをスレッド間でうっかり共有できない(少なくとも簡単にはうっかりできない程度に難しい)仕組みがあります。その仕組みの核心は標準ライブラリーの Send/Synctrait 群と言語設計によるのですが、少なくとも他の、原理的には同様の方法でマルチスレッディング機能を提供してくれているプログラミング言語と同じように、例えば C++ であれば std::shared_ptr<T> が標準ライブラリーに搭載され言語規格でスレッド安全性が保証され、つまり共有メモリーの参照カウンターの"アトミック"性が担保された状態で開発者に提供されているのと同じように Rust では Arc<T> があります。

  • Arc<T> (:="Atomically Reference Counted"); ≈ C++ の std::shared_ptr<T> に Rust らしい安全設計を加えたやつ

C++ だと std::shared_ptr の実装詳細が高度に最適化されて参照カウンターのアトミック性が不必要な場合をごにょごにょしたりとかもあるみたいですが、Rust ではそもそも「アトミック性が要らない用途≈シングルスレッド的にしか扱われない」場合は別の実装を標準ライブラリーに提供してあるよ、というのが Rc<T> の方になります。

  • Rc<T> (:="Reference Counted"); ≈ C++ の std::shared_ptr<T> からスレッド安全性とそれに必要なオーバーヘッドを捨てたやつ

すごい大雑把で"基本的"には"所有者を共有"する可能性のあるメモリーの標準ライブラリーでの提供はこの2つです。

ちなみに、チートシートでは他にもたくさんの型へオススメを分岐させています。"所有者を共有"する必要が無ければ、つまり C++ でいうと std::unique_ptr<T> に相当する Box<T> がオススメだったり、Tが整数値の場合はより低負荷な AtomicT をオススメしていたり。このチートシートはそうした「RustでTを状況に応じてどんな型で包んで扱うとよいのだったか?」を思い出させてくれる便利な絵になっています。

Ownership; 所有権

(rev.0 -> rev.1 でチートシートの2番目に現れる選択肢に切り替わりました ⇔ Threads; スレッド群)

Rustでふつーに値を扱おうとすると、値そのものと一緒にその値を扱うためのメタ情報として発生するやつ。基本的にはRustの値は束縛した変数に所有権が設定/移動される。プログラミング言語一般でいう変数aを変数bへ代入的なコードを書くと:

let a = String::from("にゃーん"); // `"にゃーん"` のデータ領域の所有権が変数 a に束縛される
let b = a; // 変数 a の "にゃーん" のデータ領域の「所有権」が b へ「移動」される; 変数 a には事実上何も残らない
let c = b.clone(); // 変数 b の所有権は残し、すっかり同じ複製のデータ領域を作りを変数 c へ束縛する; b も c も残る

基本的に、プログラミング言語一般でいう代入っぽい式を書くと、所有権がどんどん移る。もとの所有者には何の権限も無くなる。関数の呼び出しでも呼び出し元から呼び出し先へ引数の所有権は変わらず「移動」します。

fn f( param1: String )
{ /* ⚠何もしない関数に見えるかもしれないけれど */ }

fn main()
{
 let a = String::from("にゃおーん");
 f( a ); // 関数を呼んで
 let b = a; // それから a の所有権を b へ移動する(つもり)
}

関数 f は呼ばれる時に与えられた実引数の所有権を関数スコープの仮引数 param1 へ移動します。 f を呼ぶと a"にゃおーん" の所有権を失うので、その後で let b = a; しようとしても翻訳に失敗します。

fn g( param1: String ) -> String
{ param1 } // 👈 return param1; の省略形

fn main()
{
 let a = String::from("にゃおーん");
 let a = g( a ); // 関数を呼んで、 return (の値のその所有権)を新たな a へ束縛する
 let b = a; // それから a の所有権を b へ移動する(翻訳できます)
}

にゃおーん。ちなみにここまでの例では String を扱おうとしているので所有権の学習をできます。しかし、 i32 とか &str で同じことをやろうとすると、こうはならないのでアレレ?となるかもしれません。そうなる場合は Copy+Clone とか、あるいは From, Into などがどうとかモニョモニョがゲフゲフです。この記事では触りだけでいいかなって思うので続きは↓をどうぞ。

Allocate; 配置

プログラミング言語一般でいうアロケート≈メモリー確保は Stack か Heap かにわかれます。Rust Memory Container Cheat-sheet でいう Allocate もこれです。

// Rust
let a = 123; // Stack にメモリー確保されます
let unique_a = Box::new( 456 ); // Heap にメモリー確保されます。
// C++
const auto a = 123; // Stack にメモリー確保されます
const auto unique_a = std::make_unique<std::int64_t>( 456 ); // Heap にメモリー確保されます。

一般的なPC、または Arduino とか STM32 とか、そこそこリッチな組み込み系も含めた計算機ではプログラムが動作する仕組み上、 Stack と Heap では何かと扱いやストイックなレベルでの性能が変わったりします。入門、ホビー向けの入り口としてはどちらもデータを格納するメモリー領域である事は違いありませんが、プログラマー…(省略されました)。

Mutable; 可変

一般に、わりと少なくないプログラミング言語の Variable; 変数 は Mutable; 可変 を暗黙の扱いにしています。そうしたプログラミング言語らは Const; 一定≈定数 とか Mutable/Immutable; 可変/不変 ReadOnly; 読み出し専用 とかにしたい場合にそれっぽいキーワードを付けて変数のシンボルを宣言します。そうではないプログラミング言語もあり、 Rust もその1つです。プログラミング言語一般にいう変数は Rust では不変が暗黙の扱いとなります。そうしたプログラミング言語では可変にしたい場合にそれっぽいキーワードを付けて変数のシンボルを宣言します。

// Rust
let a = 123; // これは不変なので 123 からナカミを変更できません
let mut b = 456; // これは可変なのでナカミを変更できます
b = a + 789; // ナカミを a + 789 へ変更できます
println!( "{}", b ); // 912
// F#
let a = 123 // これは不変なのでry
let mutable b = 456 // これは可変なのでry
b <- a + 789 // ry
printfn "%d" b // ry
-- Haskell
let a = 123 -- これは不変というかry
b <- newIORef( 456 ) -- IOモナドで可変といえば可変…
writeIORef b ( a + 789 ) -- まあ…
print =<< readIORef b -- その…

"きほんてきに不変" なプログラミング言語で著者が扱える…というか知っているのはこの3つだけです。ほか、 C++ はじめ、多くのプログラミング言語は "きほんてきに可変" のグループです。

// C++
const auto a = 123; // 不変にしたい時にキーワードを付ける
auto b = 456; // mutable とか書かなくても可変になります
b = a + 789; // 可変なので変更できます
std::clog << b << '\n'; // 912

Interior-mutability; 内部可変性

通常は mut な何かはその内部、例えば struct のフィールドも mut だし、 mut じゃない何かはそのナカミも含め mut 化する事はできません。Rustでは翻訳時に Borrow Checker; 借用規則チェッカー がその辺りを精査してくれるので、 "実行してみたら不可能なコードでしんだ💀" のような事は起こらない(極めて起こりにくい)ようにできています。つまり、原則的には「内部可変性」なんてものはあるはずがなく、 mut なやつはナカミも mut 、そうじゃないやつはナカミもそうじゃない世界です。

ですが、諸事情により、それ=翻訳時の借用規則チェッカーではしんどい場合に対処するために、部分的に、翻訳時には借用規則をチェックせずに、実行時のオーバーヘッドは増えるものの実行時に借用規則チェックを事実上"遅延"する仕組みがあります。ちなみに手作りしようと思えば unsafe の世界でにゃんにゃんできますが、普通は手作りすることはありません。

 let a: Rc<Cell<i32>> = Rc::new(Cell::<i32>::new(123)); // a は不変の Rc 型に内部可変性を持つ Cell で i32 をぶちこんでいる
 a.set( 789 ); // Rc 部分は不変だけど、 Cell 部分は内部可変を実行時の借用規則チェックで安全性を保ったまま事実上の mut れる

↑この mut (=翻訳時に可変) せずに Cell, RefCell (=実行時にナカミだけ可変=内部可変) したい諸事情が Rust のマルチスレッディング・プログラミングでは一般的に発生します。

R/W; 読み込み/書き込み

このチートシートの絵の中では、何らかのマルチスレッディング実行中のプログラムで、ある値に対して読み/書きを行う際にデータ競合を防ぐ仕組みの Reader|Readers/Writer パターンの対応を示しています。

  • Reader/Writer = 読み込みも書き込みも"同時"に1つのスレッドからしか行わせない
  • Readers/Writer =読み込みは複数から"同時"に行わせるけど、書き込みは1つのスレッドからしか行わせない

通常、メモリー領域へのアクセス権に問題が無ければマルチスレッディング実行中のプログラムから同じメモリーアドレスを同時に読み出す処理は現代の一般的なOSとハードウェアの上では安全に処理できます。書き込みが絡む時にデータ競合が起きたりしてやばみが溢れ出す可能性があります💀。あるいは、何らかのアルゴリズムの都合上、 Readers ではなく Reader が欲しい場合も考えられます。

// Rust
use std::{ sync::{Arc, RwLock} };

fn main()
{
 let a: Arc<RwLock<i32>> = Arc::new(RwLock::<i32>::new(123));
 {
   let r0 = *a.read().unwrap(); // 読み出しロック0号
   let r1 = *a.read().unwrap(); // 読み出しロック1号
   println!("r0 = {:?}, r1 = {:?}", r0, r1); // 複数の読み出しロックを介してナカミへ同時(この例だとスレッド的には厳密には同時じゃないけど)にアクセス!
   // ここで r0, r1 は Drop で drop するから a は何もロックのない状態になります
 }
 *a.write().unwrap() = 789; // 他にロックされていないので書き込みロックを処理しナカミへ書き込みできます
}
#include <memory>
#include <shared_mutex>
#include <iostream>
int main()
{
  std::shared_ptr<std::int32_t> d = std::make_shared( 123 ); // Rust の RwLock のナカミのデータ部分に相当
  std::shared_mutex m; // Rust の RwLock の排他制御の制御機構部分に相当
  {
    auto r0 = std::shared_lock<decltype(m)>(m); // 読み出し用共有ロック0号
    auto r1 = std::shared_lock<decltype(m)>(m); // 読み出し用共有ロック1号
    std::clog << "r0 = " << &r0 << ", r1 = " << &r1 << ", d = " << d->value; // 同時に複数の読み出し用共有ロックの上でデータを読み出せます
    // ここで r0, r1 は RAII で読み出し用の共有ロックを解除します
  }
  auto w = std::lock_guard<decltype(m)>(m); // 他にロックは無いので書き込み用の排他ロックを行って
  d->value = 789; // データへ書き込みできます
}

ちなみに、 Mutex<T>RwLock<T> の何れを使うべきかについて、アルゴリズム上の最適解とは別に、処理速度、実装都合までストイックに考慮したい事がもしあれば、 OS によって実装詳細が異なる可能性を考慮した上で RwLock の僅かな実装上のオーバーヘッドと Readers が Readers である有効性、またはそもそも Mutex<T>, RwLock<T> が最適か AtomicT か、他の非標準の crate あるいは unsafe か、そんな具合の追求が必要になります。追求がそもそも必要かはさておき👽

Reference; 参照

Rust の世界では & で 参照を取得できます。翻訳のナカミは *const ポインター型と同様ですが、 Rust では Safe; 安全 な世界を好むのでポインターの Dereference; 間接参照 に unsafe が必要なポインターは通常は好んで使われる事はありません。

fn f( tabenaiyo: &String ) { println!( "f より {:?} を参照を通じて眺めています。", tabenaiyo ) }
let a: String = String::from("ばうわう");
let ar: &String = &a;
f( ar ); // 参照を渡しても所有権は渡らないので a が f に食べられたりしません
println!( "まだ a = {:?} の所有権は失われていません。", a )

原則的に参照は & の事なのですが、チートシートには CellRefCell も Reference からの <val> または <ref> で導いています。どちらも先に出てきた Interior-mutability を実装した struct です。

Cell は値の "移動" によって内部可変性を実装するため <T>Copy 可能な "値" 向けのコンテナーで、 i32Copy trait を実装した何かを扱うのに"適した"コンテナーです。実は可変というか小さなIOモナド機構を内部に持つというか。

RefCell は可変な "参照" を .borrow_mut() -> RefMut<T>&mut T で取り出せるように実装したコンテナーで、実際のところはどんな <T> でも放り込めますが Cell では扱えない型に内部可変性を与えたい場合に"適した"コンテナーです。

use std::{ rc::Rc, cell::{Cell, RefCell} };
fn main()
{
 // Cell, RefCell がない世界
 let a: Rc<i32> = Rc::new(1); // Rc は内部可変性がないので Cell, RefCell を挟まないとナカミは immutable
 println!( "*a = {}", *a ); // deref してナカミを読み出す…というか Copy で Clone を取得する事はできます
 //let mut a: Rc<&mut i32> = Rc::new(&2); // こういう事を頑張ろうとしてもダメです
//*
 // Cell がある世界
 let a: Rc<Cell<i32>> = Rc::new(Cell::new(2)); // Rc だけどナカミは Cell なので Cell のナカミは可変性あり
 a.set( a.get() * 100 ); // Rc は Deref でナカミの Cell の .set .get を透過的に呼べる。 Cell は .get .set 関数で内部可変性を実装しています。
 println!( "a.get() = {}", a.get() ); // 200

 let v: Rc<Cell<Vec<i32>>> = Rc::new(Cell::new(vec![1,2,3])); // T が Copy を実装しない Vec でも作る事はできるけれど
 //v.get(); // ムリ
 v.set(vec![7,8,9]); // これは翻訳はまあできるんだけど…
 // dbg!(&v); // とにかくどうやっても Copy trait を実装していない T にはムリ!!
//*/
 // RefCell がある世界
 let v: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(vec![1,2,3])); // T が Copy を実装しない Vec で作っても
 //v.borrow_mut().append( &mut v.borrow().iter().map(|i| i + 1 ).collect::<Vec<_>>() ); // これは実行時の BorrowChecker で死ぬのでダメだけど
 let mut vv = v.borrow().iter().map(|i| i + 1 ).collect::<Vec<_>>(); // こうして
 v.borrow_mut().append( &mut vv ); // こうすると
 println!( "v = {:?}", v.borrow() ); // 実行時の BorrowChecker もパスして平穏無事に動きます。
}

doc からは実装詳細的な意味でのナカミも src リンクをぽちっとなするだけで辿れるのでそちらに興味が湧いてしまったら doc から辿るとよいかもしれません。

Type; 型

<bool|int> or <any>

チートシートでは <bool|int><any> で分けています。その先を見ると <bool|int> の場合は AtomicT へ、 <any> の場合は Mutex<T> へ導いています。Rust にも CPU と OS が Atomic-operation; 不可分操作 をサポートする場合に言語標準ライブラリーのレベルで共通で簡単に使える AtomicBool, AtomicI8, AtomicI16, AtomicI32, AtomicI64, AtomicIsize, AtomicPtr, AtomicU8, AtomicU16, AtomicU32, AtomicU64, AtomicUsize があります。これらを表現の便宜上まとめて AtomicT と表現しています。

原理上、CPU と OS が AtomicT をサポートできている現在一般的なPCでは AtomicTMutex による実装に比べて処理性能上の大きな利点があります。そのため、 AtomicTT が限定的な数値型のみではありますが、使える場合は使いましょうという誘導です。

<Copy-able> or <any>

Interior-mutability で触れた選択肢の T がどんな型かによる分岐です。<any> つまり何でも RefCell<T> で実行時の BorrowChecker を介して &mut を取り出す仕組みと <Copy-able> つまり Copy trait が実装された T なら巧いこと扱える Cell<T> のどちらを選ぼうか、という部分です。

そのた、ほそく

チートシートの絵では「チートシートを欲する時に何を確認したいか」という事に注目していて、実は右側に並んでいるのは厳密には Type; 型 ではなかったりします。例えば mut T は型が mut なのではなく、 Rust の翻訳時に所有権などメタデータとして mut が Declare; 宣言 されているかみたいな扱いで、 mut T それ自体は型として存在はできません。

Mutex, AtomicT, RwLock の実効性の評価は Rust に限らず高級ながら比較的低級な仕組みを扱うのが得意なプログラミング言語ではしばしば性能評価と議論の対象になります。気になればぐぐられたら楽しいと思います。

usagi
LOVE 🍣 🍵 🌶️ 🍎 🍟 🥖 🧀 LANG 🇯🇵 🇺🇸 TECH C++ C# Software | IoT Hardware | Civil Engineering
https://usagi.network
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした