DanLevy.net

クイズ: Rustのメモリ管理の基礎

(借用)自分をチェックして、自分を壊す前に!🦀

Rustのメモリ管理スキルを試す準備はできましたか?🦀

このクイズでは、Rustの所有権システム、借用ルール、ライフタイム、スマートポインタに関する理解を問います。

注意: 質問は約50桁幅でフォーマットされており、すべてのデバイスで読みやすくなっています。(改善提案歓迎!)

熟練のRustaceanでも、メモリ管理を始めたばかりでも、このクイズで知識を強化できます。さあ、始めましょう! 🦀

このコードを実行するとどうなりますか?出力またはエラーを予測してみてください:

fn main() {
let philosopher =
String::from("Zeno of Citium");
let greeting = philosopher;
println!("Hello, {}!", philosopher);
}

このコードはRustの所有権ルールによりコンパイルに失敗します。philosophergreetingに代入すると、Stringの所有権がgreetingにムーブされます。このムーブの後、philosopherは使用できなくなります。

修正方法は3つあります:

  1. 文字列をクローンする(新しいコピーを作成):
let greeting = philosopher.clone();
  1. 参照を使う(値を借用する):
let greeting = &philosopher;
  1. 文字列スライスを使う(文字列の一部を借用する):
let greeting = &philosopher[..];

各解決策には異なるユースケースとパフォーマンスへの影響があります。クローンはより高価ですが所有権を得られます。一方、参照はより安価ですがライフタイムの制約があります。

このコードを実行するとどうなりますか?所有権の移動について考えてみましょう:

fn take_knowledge(knowledge: String) {
println!("Knowledge: {}", knowledge);
}
fn main() {
let wisdom = String::from("know thyself");
take_knowledge(wisdom);
// What happens to our wisdom?
println!("Do you {}", wisdom);
}

このコードはコンパイルに失敗します。なぜなら、wisdomの所有権がtake_knowledgeに移動したため、その後使用できなくなるからです。

この問題を修正するには3つの方法があります:

  1. 参照で渡す(値を借用する):
fn borrow_it(text: &String) {
println!("Inside: {}", text);
}
borrow_it(&wisdom); // Now wisdom can be used after
  1. 値をクローンする(新しいコピーを作成する):
take_knowledge(wisdom.clone()); // Original wisdom remains valid
  1. 関数から所有権を返す:
fn take_and_return(text: String) -> String {
println!("Inside: {}", text);
text // Return ownership back
}
let wisdom = take_and_return(wisdom); // Reassign returned ownership

各アプローチには異なるユースケースがあります:

  • 参照:最も効率的だが、ライフタイム管理が必要
  • クローン:シンプルだが、コストがかかる可能性がある
  • 所有権の返却:値を変換するのに便利

ベストプラクティス:所有権の移動が必要でない限り、参照を使用してください。

複数の可変参照があるとどうなるでしょうか?

fn main() {
let mut wisdom = String::from("He who laughs at");
let ref1 = &mut wisdom; // First mutable borrow
let ref2 = &mut wisdom; // Second mutable borrow
ref1.push_str(" himself never runs");
ref2.push_str(" out of things to laugh at.");
}

Rustの可変参照に関するルールを考えてみてください。

このコードはRustの基本的な借用ルールに違反しています:

  • 値に対する可変参照は同時に1つだけ
  • または不変参照はいくつでも可能
  • 参照は参照先より長生きしてはいけません

修正方法は以下の通りです:

  1. スコープを順番に使う:
let mut wisdom = String::from("He who laughs at");
{
let ref1 = &mut wisdom;
ref1.push_str(" himself never runs");
} // ref1 goes out of scope
let ref2 = &mut wisdom; // Now this is valid
ref2.push_str(" out of things to laugh at.");
  1. または、1回の借用で文字列を変更する:
let mut wisdom = String::from("He who laughs at");
let ref1 = &mut wisdom;
ref1.push_str(" himself never runs out of things to laugh at.");

これらのルールはコンパイル時にデータ競合を防ぎ、Rustをデフォルトでスレッドセーフにします。

よくある落とし穴:クローンを避けるため、または同じ値の異なる部分を同時に変更するために、複数の可変参照を使おうとすること。

このコードはコンパイルされますか? コンパイルされる場合、その理由は? されない場合、何が問題ですか?

fn first_word(s: &str) -> &str { // No explicit lifetimes?
match s.find(' ') {
Some(pos) => &s[0..pos],
None => s,
}
}
fn main() {
let name = String::from("Seneca the Younger");
let first = first_word(&name);
println!("Hello, {}", first);
}

このコードは、Rustのライフタイム省略ルールのおかげで正常にコンパイルされます。 これらのルールにより、コンパイラは一般的なパターンでライフタイムを自動的に推論できます。

3つのライフタイム省略ルールは以下の通りです:

  1. 各パラメータは独自のライフタイムパラメータを取得する
  2. 入力ライフタイムパラメータが1つだけの場合、そのライフタイムがすべての出力ライフタイムパラメータに割り当てられる
  3. 複数の入力ライフタイムパラメータがあるが、そのうちの1つが&selfまたは&mut selfの場合、selfのライフタイムがすべての出力ライフタイムパラメータに割り当てられる

この関数は以下と同等です:

fn first_word<'a>(s: &'a str) -> &'a str {
// ... same implementation
}

省略が機能する一般的なパターン:

// These don't need explicit lifetimes
fn get_str(s: &str) -> &str { s }
fn get_first(s: &str) -> &str { &s[0..1] }
// These would need explicit lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

ベストプラクティス:可能な場合は省略を活用しましょう。ただし、明示的なライフタイムが必要な場合を理解しておいてください。

この再帰的な型定義の何が問題ですか?

#[derive(Debug)]
enum CatList {
Cons(i32, CatList), // Recursive without indirection
Nil,
}
fn main() {
let catlist = CatList::Cons(1,
CatList::Cons(2,
CatList::Cons(3,
CatList::Nil)));
}

このコードは、コンパイラがコンパイル時にCatListのサイズを決定できないために失敗します。型の再帰的な性質により、無限に大きくなる可能性があります!

以下がBox<T>を使用した修正方法です:

#[derive(Debug)]
enum CatList {
Cons(i32, Box<CatList>), // Box provides a fixed-size pointer
Nil,
}
fn main() {
let catlist = CatList::Cons(1,
Box::new(CatList::Cons(2,
Box::new(CatList::Cons(3,
Box::new(CatList::Nil))))));
}

なぜBox<T>が機能するのか:

  1. Boxは固定サイズのポインタを提供します(64ビットシステムでは通常8バイト)
  2. 実際のデータはヒープに格納されます
  3. コンパイラは割り当てるべき正確なサイズを把握できるようになります

Box<T>の一般的な使用例:

  • 再帰的なデータ構造(連結リスト、ツリー)
  • ヒープに確実に割り当てたい大きなデータ
  • 動的ディスパッチが必要なトレイトオブジェクト

ベストプラクティス:次の場合にBox<T>を使用します:

  • 再帰的な型
  • ヒープ割り当てを確実に行いたい場合
  • コピーせずに大きなデータを移動したい場合

このコードは何を出力するでしょうか?慎重に数えてください!

use std::rc::Rc;
fn main() {
let text = Rc::new(String::from("Meditations")); // Count: 1
let marcus = Rc::clone(&text); // What happens here?
let aurelius = Rc::clone(&text); // And here?
println!(
"Reference count: {}",
Rc::strong_count(&text)
);
}

Rcの動作を分解してみましょう:

  1. Rc::new() による初期作成: カウント = 1
  2. marcus への最初のクローン: カウント = 2
  3. aurelius への2番目のクローン: カウント = 3

重要なRcの特性:

use std::rc::Rc;
fn demonstrate_rc() {
let original = Rc::new(String::from("Shared"));
println!("Count after creation: {}", Rc::strong_count(&original)); // 1
{
let copy = Rc::clone(&original);
println!("Count inside scope: {}", Rc::strong_count(&original)); // 2
} // copy is dropped here
println!("Count after scope: {}", Rc::strong_count(&original)); // 1
}

重要なポイント:

  • Rc::clone() は軽量です - カウンタをインクリメントするだけです
  • Rc はシングルスレッドのシナリオのみで使用します
  • 最後の参照がドロップされると、データはクリーンアップされます
  • 参照サイクルを防ぐために Weak 参照を使用します

ベストプラクティス:

  • 共有所有権が必要な場合は Rc を使用します
  • スレッドセーフなシナリオには Arc を検討します
  • 参照サイクルを作成しないようにします

この構造体定義はコンパイルされますか?その理由は?

struct Philosopher {
name: &str, // Reference without lifetime
quote: &str, // Another reference without lifetime
}
fn main() {
let phil = Philosopher {
name: "Seneca",
quote: "Luck happens when preparation meets opportunity",
};
}

コードは失敗します。なぜなら、参照を含む構造体はライフタイムを指定する必要があるからです。修正方法は次の通りです:

// Single lifetime parameter
struct Philosopher<'a> {
name: &'a str,
quote: &'a str,
}
// Or different lifetimes if needed
struct PhilosopherFlex<'n, 'q> {
name: &'n str,
quote: &'q str,
}

一般的なパターン:

// Own the data instead
struct PhilosopherOwned {
name: String,
quote: String,
}
// Mixed ownership
struct PhilosopherMixed<'a> {
name: String, // Owned
quote: &'a str, // Borrowed
}

ベストプラクティス:

  1. データを無期限に保存する必要がある場合は所有型(String)を使用する
  2. 構造体のライフタイムがデータよりも明らかに短い場合は参照を使用する
  3. 参照が異なるライフタイムを持つ可能性がある場合は複数のライフタイムパラメータを検討する
  4. 複雑な構造体ではライフタイムの関係を文書化する

2つの文字列スライスのうち長い方を返すこの関数はどうなるでしょうか?

fn longest(text1: &str, text2: &str) -> &str {
if text1.len() > text2.len() {
text1 // Returning a reference, but which lifetime?
} else {
text2 // Could be this reference instead
}
}
fn main() {
println!("{}", longest(
"Seneca the Younger",
"Marcus Aurelius"
));
}

このコードは、コンパイラが入力と出力のライフタイムの関係を判断できないため失敗します。その理由と修正方法は次のとおりです。

// Correct version with explicit lifetime annotation
fn longest<'a>(text1: &'a str, text2: &'a str) -> &'a str {
if text1.len() > text2.len() {
text1
} else {
text2
}
}
// Alternative with different lifetimes
fn longest_flex<'a, 'b>(text1: &'a str, text2: &'b str) -> &'a str {
if text1.len() > text2.len() {
text1
} else {
text2.to_string().as_str() // Won't compile! Shows why we need same lifetime
}
}

なぜここでライフタイムが必要なのか:

  1. 複数の入力参照が異なるライフタイムを持つ可能性がある
  2. 戻り値は両方の入力と同じだけ生存しなければならない
  3. コンパイラはこれらの関係を検証する必要がある

一般的なパターン:

// Single input reference - elision works
fn first_word(s: &str) -> &str { /* ... */ }
// Multiple references, same lifetime needed
fn compare_str<'a>(s1: &'a str, s2: &'a str) -> &'a str { /* ... */ }
// Different lifetimes possible
fn combine<'a, 'b>(s1: &'a str, s2: &'b str) -> String { /* ... */ }

ベストプラクティス:

  1. 可能な場合はライフタイムの省略に任せる
  2. 関係を明確にする必要がある場合は明示的なライフタイムを使用する
  3. ライフタイムの複雑さを避けるために所有型を返すことを検討する
  4. 複雑なライフタイムの関係を文書化する

このコードを実行するとどうなりますか?

use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
let _borrow1 = data.borrow_mut(); // First mutable borrow
let _borrow2 = data.borrow_mut(); // Second mutable borrow
println!("Value: {}", _borrow2);
}

RefCellは内部可変性を提供しますが、Rustの借用ルールを実行時に強制します:

use std::cell::RefCell;
fn demonstrate_refcell() {
let data = RefCell::new(42);
// Correct way to use RefCell
{
let mut first = data.borrow_mut();
*first += 1;
} // first is dropped here
// Now we can borrow again
let second = data.borrow_mut();
// Or multiple immutable borrows
let read1 = data.borrow();
let read2 = data.borrow(); // This is OK
}

主要な概念:

  1. RefCellは借用チェックを実行時に移動する
  2. ルールに違反するとパニックを引き起こす可能性がある
  3. 内部可変性パターンに有用

一般的なユースケース:

  • テストでのモックオブジェクト
  • 自己参照構造体の実装
  • 共有参照の背後でデータを変更する必要がある場合

ベストプラクティス:

  1. 可能な限りコンパイル時の借用を優先する
  2. RefCellの借用を狭いスコープに保つ
  3. drop()を使用して明示的に借用を終了することを検討する
  4. 内部可変性が必要な場合にRefCellを使用する

このコードは何を出力しますか?

use std::cell::Cell;
fn main() {
let life = Cell::new(42);
let meaning = &life; // Shared reference
println!("{}", life.get()); // What prints here?
meaning.set(43); // Mutation through shared ref
println!("{}", life.get()); // And here?
}

Cell と RefCell は内部可変性のために異なる目的を持っています:

use std::cell::{Cell, RefCell};
// Cell for Copy types
struct Counter {
count: Cell<i32>,
}
impl Counter {
fn increment(&self) {
self.count.set(self.count.get() + 1);
}
}
// RefCell for non-Copy types
struct Logger {
messages: RefCell<Vec<String>>,
}
impl Logger {
fn log(&self, msg: &str) {
self.messages.borrow_mut().push(msg.to_string());
}
}

主な違い:

  1. Cell:
  • Copy 型に最適
  • 借用 API はない
  • 常に値をコピーまたはムーブする
  1. RefCell:
  • 任意の型で動作する
  • 借用 API がある
  • 実行時に借用チェックを行う

ベストプラクティス:

  1. 単純な Copy 型(数値、bool など)には Cell を使う
  2. 内容を借用する必要がある場合は RefCell を使う
  3. Cell/RefCell による変更は最小限に抑える
  4. 内部可変性が必要な理由を文書化する

RustでRc(参照カウント)をいつ使うべきですか?

次の例を考えてみましょう:

use std::rc::Rc;
struct SharedConfig {
name: String,
value: i32,
}
fn main() {
let config = Rc::new(SharedConfig {
name: "settings".to_string(),
value: 42,
});
let config2 = Rc::clone(&config);
// Both config and config2 share ownership
}

Rc(参照カウント)は、共有所有権が必要なシングルスレッドシナリオ向けに設計されています。

一般的な使用例:

use std::rc::Rc;
use std::cell::RefCell;
// Shared ownership in data structures
struct Node {
next: Option<Rc<Node>>,
value: i32,
}
// Combining with interior mutability
struct SharedState {
data: Rc<RefCell<Vec<String>>>,
}
// Multiple owners of same data
let original = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&original);
let clone2 = Rc::clone(&original);

重要なポイント:

  1. Rcを使うべき時:
  • コードの複数の部分が所有権を必要とする場合
  • 共有がシングルスレッドであることがわかっている場合
  • ライフタイムが静的に決定できない場合
  1. 代わりにArcを使うべき時:
  • スレッドセーフな共有が必要な場合
  • 複数のスレッドが所有権を必要とする場合
  1. Rcの制限:
  • スレッドセーフではない
  • わずかなランタイムオーバーヘッド
  • 参照サイクルを自動的に解消できない

ベストプラクティス:

  1. 可能な限り単一所有権を優先する
  2. シングルスレッドの共有所有権にはRcを使う
  3. マルチスレッドシナリオにはArcを使う
  4. 参照サイクルを防ぐためにWeakと組み合わせる

RustにおけるRefCellとRwLockの主な違いは何ですか?

以下の例を考えてみましょう:

use std::cell::RefCell;
use std::sync::RwLock;
// Example 1
let data = RefCell::new(vec![1, 2, 3]);
let borrowed = data.borrow_mut();
// Example 2
let shared = RwLock::new(vec![1, 2, 3]);
let locked = shared.write().unwrap();

RefCellとRwLockは似た目的を持つが、異なるコンテキストで使われる:

// Single-threaded scenario with RefCell
use std::cell::RefCell;
struct SingleThreaded {
data: RefCell<Vec<i32>>,
}
impl SingleThreaded {
fn modify(&self) {
self.data.borrow_mut().push(42);
}
}
// Multi-threaded scenario with RwLock
use std::sync::RwLock;
struct ThreadSafe {
data: RwLock<Vec<i32>>,
}
impl ThreadSafe {
fn modify(&self) {
self.data.write().unwrap().push(42);
}
}

主な違い:

  1. RefCell:
  • シングルスレッドのみ
  • 同期オーバーヘッドなし
  • 借用違反でパニック
  1. RwLock:
  • スレッドセーフ
  • 同期オーバーヘッドあり
  • パニックではなくスレッドをブロック可能

ベストプラクティス:

  1. シングルスレッドの内部可変性にはRefCellを使う
  2. スレッドセーフが必要な場合はRwLockを使う
  3. よりシンプルなスレッドセーフ可変性にはMutexを検討する
  4. スレッドセーフ要件を明確に文書化する

このコードを実行するとどうなりますか?

use std::sync::{Arc, Mutex};
fn main() {
let lock = Arc::new(Mutex::new(42));
let lock2 = Arc::clone(&lock);
let _guard1 = lock.lock().unwrap(); // First lock
let _guard2 = lock2.lock().unwrap(); // Second lock attempt
println!("Value: {}", _guard2);
}

このコードは古典的なデッドロックのシナリオを示しています。修正方法は以下の通りです:

use std::sync::{Arc, Mutex};
// Correct way - Release lock before acquiring it again
fn safe_mutex() {
let lock = Arc::new(Mutex::new(42));
{
let mut data = lock.lock().unwrap();
*data += 1;
} // Lock is released here
// Now we can acquire it again
let data2 = lock.lock().unwrap();
println!("Value: {}", data2);
}
// Using multiple mutexes safely
fn multiple_mutexes() {
let lock1 = Arc::new(Mutex::new(42));
let lock2 = Arc::new(Mutex::new(43));
// Always acquire locks in the same order
let guard1 = lock1.lock().unwrap();
let guard2 = lock2.lock().unwrap();
}

デッドロックを防ぐためのベストプラクティス:

  1. クリティカルセクションを小さく保つ
  2. スコープを使ってロックを速やかに解放する
  3. 複数のロックを一貫した順序で取得する
  4. パフォーマンス向上のためにparking_lot::Mutexを使用する
  5. 読み取りが多いワークロードにはRwLockを検討する

一般的なパターン:

// Thread-safe counter
struct Counter {
count: Arc<Mutex<i32>>,
}
impl Counter {
fn increment(&self) {
let mut count = self.count.lock().unwrap();
*count += 1;
} // Lock automatically released here
}

弱参照を使ったこのコードを実行するとどうなりますか?

use std::rc::{Rc, Weak};
fn main() {
let data = Rc::new(String::from("Wisdom"));
let weak = Rc::downgrade(&data); // Create weak reference
drop(data); // Drop strong reference
println!("Value: {:?}", weak.upgrade());
}

弱参照は対象の解放を妨げません。詳細な例を示します:

use std::rc::{Rc, Weak};
use std::cell::RefCell;
// Parent-child tree structure avoiding reference cycles
struct Node {
next: Option<Rc<Node>>,
parent: RefCell<Weak<Node>>, // Weak to prevent cycles
value: i32,
}
impl Node {
fn new(value: i32) -> Rc<Node> {
Rc::new(Node {
next: None,
parent: RefCell::new(Weak::new()),
value,
})
}
fn set_parent(&self, parent: &Rc<Node>) {
*self.parent.borrow_mut() = Rc::downgrade(parent);
}
fn get_parent(&self) -> Option<Rc<Node>> {
self.parent.borrow().upgrade()
}
}

一般的な使用例:

  1. エントリをクリアできるキャッシュのような構造
  2. 親参照を持つツリー構造
  3. サブジェクトがドロップされる可能性のあるオブザーバーパターン
  4. 複雑なデータ構造における参照サイクルの解消

ベストプラクティス:

  1. オプショナルな関係には弱参照を使用する
  2. 使用前に upgrade() の結果を確認する
  3. 所有権の関係を明確に文書化する
  4. 単純なケースではインデックスなどの代替手段を検討する

このRAIIの例では、ファイルハンドルはどうなりますか?

use std::fs::File;
struct FileWrapper {
file: File,
}
fn main() {
let file = File::create("test.txt").unwrap();
let wrapper = FileWrapper { file };
// ... use wrapper ...
// No Drop implementation
}

RustのRAIIはリソースが適切に管理されることを保証します。この例では、FileWrapperはファイルハンドルを閉じるためにカスタムのDrop実装を必要としません。そのFileフィールドは、ラッパーがスコープ外になると自動的にドロップされます。

ラッパー自体がフィールドのドロップ以外に追加のクリーンアップ動作を持つ場合にのみ、Dropを実装します:

use std::fs::File;
use std::io::{self, Write};
struct FileWrapper {
file: File,
path: String,
}
impl FileWrapper {
fn new(path: &str) -> io::Result<FileWrapper> {
Ok(FileWrapper {
file: File::create(path)?,
path: path.to_string(),
})
}
fn write(&mut self, content: &str) -> io::Result<()> {
self.file.write_all(content.as_bytes())
}
}
impl Drop for FileWrapper {
fn drop(&mut self) {
// Ensure file is properly closed
// Could also do cleanup like deletion
println!("Closing file: {}", self.path);
}
}

RAIIパターン:

  1. コンストラクタがリソースを取得する
  2. メソッドがリソースを安全に使用する
  3. 所有者がスコープ外になるとフィールドが自動的にドロップされる
  4. 必要な場合にカスタムDropが追加のクリーンアップを行う
  5. エラー伝播に?を使用する

ベストプラクティス:

  1. 標準ライブラリのDrop実装がすでにリソースをモデル化している場合はそれに依存する
  2. リソース管理をシンプルかつ明白に保つ
  3. 可能な場合は標準ライブラリの型を使用する
  4. クリーンアップ動作を文書化する
  5. スコープ付き操作にはガードパターンの使用を検討する

このPhilosophy構造体をクローンするとどうなりますか?

#[derive(Clone)]
struct Philosophy {
school: String,
founder: String,
}
fn main() {
let stoicism = Philosophy {
school: String::from("Stoicism"),
founder: String::from("Zeno of Citium")
};
let new_school = stoicism.clone();
println!("{} - {}",
stoicism.school, new_school.school);
}

CopyとCloneの違いを詳しく見てみましょう:

// Types that can be Copy
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
// Types that can only be Clone
#[derive(Clone)]
struct ComplexData {
name: String, // String can't be Copy
points: Vec<i32> // Vec can't be Copy
}
// Manual implementation example
#[derive(Debug)]
struct Custom {
data: Vec<i32>,
identifier: u32,
}
impl Clone for Custom {
fn clone(&self) -> Self {
Custom {
data: self.data.clone(),
identifier: self.identifier, // Copy type
}
}
}

主な違い:

  1. Copy:
  • 暗黙的、ビット単位のコピー
  • Copy-safeである必要がある(ヒープ割り当てなし)
  • 通常は小さなスタックのみの型に使用
  1. Clone:
  • 明示的、潜在的にディープコピー
  • ヒープ割り当てを扱える
  • より柔軟だがコストがかかる可能性あり

ベストプラクティス:

  1. 小さなスタックのみの型にはCopyを実装する
  2. 所有リソースを持つ型にはCloneを使用する
  3. Cloneのパフォーマンスへの影響を文書化する
  4. 最適化のためにカスタムClone実装を検討する
  5. 自動導出には注意する

典型的な現在の64ビットRustターゲットにおいて、この構造体のサイズはいくつですか?

struct Metadata {
id: u32, // How many bytes?
name: String, // How many bytes?
active: bool // How many bytes + padding?
}

構造体のメモリレイアウトと最適化を分解してみましょう:

// Typical current 64-bit Rust layout: 32 bytes
struct Metadata {
id: u32, // 4 bytes
name: String, // 24 bytes on 64-bit systems
active: bool // 1 byte + padding/alignment
}
// Reordering fields may reduce padding for repr(C) structs,
// but default Rust layout is not a stable ABI guarantee.
struct OptimizedMetadata {
name: String, // 24 bytes
id: u32, // 4 bytes
active: bool // 1 byte + 3 padding
}
// Further optimization with packing
#[repr(packed)]
struct PackedMetadata {
id: u32,
active: bool,
name: String,
}

メモリレイアウトの考慮事項:

  1. アライメント要件:
  • u32: 4バイトアライメント
  • String: 一般的な64ビットターゲットでは8バイトアライメント、24バイトサイズ
  • bool: 1バイトアライメント
  1. フィールド順序の戦略:
  • 同じサイズのフィールドをグループ化する
  • 大きいアライメントを先に配置する
  • キャッシュラインの最適化を考慮する

ベストプラクティス:

  1. FFIや安定したレイアウトの前提が必要な場合は、適切なrepr(...)を使用する
  2. 適切な整数サイズを使用する
  3. オプショナルフィールドにはOptionの使用を検討する
  4. サイズが重要な構造体はstd::mem::size_ofで測定する
  5. #[repr(packed)]は慎重に使用する - パフォーマンスに影響を与える可能性がある

これら2つの実装のパフォーマンスはどのように比較されますか?

// Implementation A: Iterator
fn sum_iterator(v: &[i32]) -> i32 {
v.iter().fold(0, |acc, &x| acc + x)
}
// Implementation B: Raw loop
fn sum_loop(v: &[i32]) -> i32 {
let mut sum = 0;
for i in 0..v.len() {
sum += v[i];
}
sum
}

Rustのゼロコスト抽象化は同等の効率的なコードにコンパイルされます:

use std::ops::Range;
// High-level abstraction
trait ZeroCost {
fn process(&self) -> u32;
}
impl ZeroCost for Range<u32> {
fn process(&self) -> u32 {
self.fold(0, |acc, x| acc + x)
}
}
// Compiles to essentially the same code as:
fn manual_process(range: Range<u32>) -> u32 {
let mut sum = 0;
let mut i = range.start;
while i < range.end {
sum += i;
i += 1;
}
sum
}
// Even more abstractions, still zero-cost
fn complex_processing<T>(data: &[T]) -> u32
where T: AsRef<str> {
data.iter()
.map(|s| s.as_ref().len())
.filter(|&n| n > 3)
.fold(0, |acc, n| acc + n as u32)
}

主要な原則:

  1. 使わないものにはコストを払わない
  2. 使うものは手書きコードより優れている

ベストプラクティス:

  1. 高レベル抽象化を自由に使う
  2. コンパイラの最適化を信頼する
  3. 最適化の前にプロファイリングする
  4. まず可読性に集中する
  5. 恐れずにイテレータとクロージャを使う

クイズに挑戦していただきありがとうございます!Rustの知識を試すのが楽しかったなら、他のプログラミングチャレンジもぜひご覧ください!🧠

Rustスキルをさらに向上させたいですか? おすすめのリソースを紹介します: