こんにちは、ペイルドの森です。ペイルドでは創業以来バックエンドの開発言語にRustを採用してきたため、Rustという言語そのものが持つ課題とその解消、クレートの流行り廃りなどの歴史を共に歩んできました。その中でも最も苦しんだものの一つに遅延初期化があります。
その遅延初期化のための機能が、2024年7月リリース予定のRust 1.80.0で安定化されるということで、本機能について、その歴史とともに見ていきます。
目次
遅延初期化とは何か、何のためにするのか
まず遅延初期化(Lazy Initialization)についておさらいしておきましょう。遅延初期化とは、プログラムにおいてリソースの初期化をそのリソースが実際に使用されるまで遅らせる技法です。これにより、プログラムの起動時間を短縮し、必要なリソースが使用されるまで無駄な計算やメモリ消費を避けることができます。遅延初期化を検討する際に気をつけるべき観点は以下の3点となります。
パフォーマンス向上のため
遅延初期化を用いることで、プログラムの初期起動時に行われる不要な計算やリソースの割り当てを回避できます。これにより、プログラムの起動時間が短縮され、メモリ使用量が抑えられます。
リソースの効率的な管理のため
特にメモリやファイルハンドルなど、限られたリソースの使用を効率化するために、遅延初期化は有用です。必要になった時点で初めてリソースを割り当てることで、無駄を減らします。
循環依存の回避のため
遅延初期化は、複雑な依存関係を持つオブジェクトの初期化時に循環依存を回避するためにも使用されます。あるオブジェクトが他のオブジェクトに依存している場合、遅延初期化により初期化順序の問題を解消できます。特に現代の複雑なプログラミングにおいては最も重要な観点かもしれません。
Rustと遅延初期化の歴史
さて遅延初期化についておさらいしたところで、Rustと遅延初期化の歴史を振り返ってみましょう。
lazy_static
クレートの登場と、抱えていた課題
最初はなんといっても lazy_static
です。lazy_static
クレートは、2014年11月23日に最初のバージョンがリリースされました。これにより、Rustでのグローバル変数の遅延初期化が可能となり、多くのプロジェクトで広く採用されるようになりました。
#[macro_use] extern crate lazy_static; use std::collections::HashMap; use std::sync::Mutex; lazy_static! { static ref DATA: Mutex<HashMap<i32, String>> = { let mut m = HashMap::new(); m.insert(13, "Spica".to_string()); m.insert(74, "Hoyten".to_string()); Mutex::new(m) }; } fn main() { let data = DATA.lock().unwrap(); println!("{:?}", data.get(&13)); }
しかし、この lazy_static
クレートはいくつかの課題を抱えていました。
マクロ依存性
lazy_static
はマクロベースで実装されているため、コードの可読性や保守性が低下することがあります。というか低下します。
スレッドセーフでない初期化のリスク
複数のスレッドが同時に初期化コードにアクセスすると、初期化が複数回実行される可能性があります。これにより、デッドロックや未定義の動作が発生するリスクがありました。
型の制約
lazy_static
では、静的変数として使用する型が Sync トレイトを実装している必要があります。これは、スレッドセーフでない型を使用したい場合には非常に悩ましい制約です。また、型が複雑になると、直感的ではない型注釈が必要になることがあります。
初期化のタイミング制御
初期化のタイミングを細かく制御することが難しい場合があります。特に、複数の静的変数が相互に依存している場合、その初期化順序の管理は至難を極めます。
once_cell
クレートの登場によって何が解決されたのか?
これらの課題を解決するために、once_cell
クレートが登場したのが2020年です。
use once_cell::sync::Lazy; use std::collections::HashMap; use std::sync::Mutex; static DATA: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| { let mut m = HashMap::new(); m.insert(13, "Spica".to_string()); m.insert(74, "Hoyten".to_string()); Mutex::new(m) }); fn main() { let data = DATA.lock().unwrap(); println!("{:?}", data.get(&13)); }
マクロが消えました!最高!さらにonce_cell
の利便性とパフォーマンスが徐々に認知され、特に新しいプロジェクトや高負荷に耐えることを要求されるシナリオでの採用が進みました。
ここで、改めてonce_cell
の特徴を以下にまとめておきましょう。
マクロを使わないシンプルなAPI
once_cell
はマクロを使用せず、シンプルで直感的なAPIを提供してくれました。
スレッドセーフな初期化
OnceCell
や OnceLock
は、スレッドセーフな初期化を保証し、デッドロックや複数回初期化のリスクを大幅に軽減してくれました。
型の柔軟性
once_cell
では、Sync
トレイトを必要とせず、さまざまな型を使用することができます!素晴らしい!
初期化タイミングの明示的な制御
OnceCell
や OnceLock
は、初期化関数を明示的に渡すことで、初期化のタイミングを細かく制御できます。これにより、相互依存する静的変数の初期化順序を適切に管理できるようになりました。
LazyCell
/LazyLock
の標準ライブラリ化提案
2023年には公式APIに遅延初期化を取り込むために、once_cell
クレートを基にLazyCell
と LazyLock
がRustの標準ライブラリに統合されることが提案されました。これらの機能を標準ライブラリに統合することで、開発者が追加の依存関係なしにこれらの利点を享受できるようにすることが目的でした。
これにより、Rustのエコシステム全体で一貫した遅延初期化の手法が提供されることになり、また once_cell
の機能をベースに、さらに最適化された実装が提供されることで、パフォーマンスと安全性の両面でも改良が進められました。
そして安定化へ
そしてついに2024/7リリース予定の1.80.0で、LazyCell
/LazyLock
が安定化される予定です!
use std::cell::LazyCell; struct Config { data: LazyCell<String>, } impl Config { fn new() -> Self { Config { data: LazyCell::new(|| "Initial Data".to_string()), } } fn get_data(&self) -> &str { self.data.get() } } fn main() { let config = Config::new(); println!("{}", config.get_data()); }
use std::sync::LazyLock; use std::collections::HashMap; use std::sync::Mutex; static GLOBAL_DATA: LazyLock<Mutex<HashMap<i32, String>>> = LazyLock::new(|| { let mut m = HashMap::new(); m.insert(13, "Spica".to_string()); m.insert(74, "Hoyten".to_string()); Mutex::new(m) }); fn main() { let data = GLOBAL_DATA.lock().unwrap(); println!("{:?}", data.get(&13)); }
シンプルなAPI
LazyCell
と LazyLock
は once_cell
ベースのシンプルで直感的なAPIを提供します。
さらに once_cell
と比べても、初期化時に関数を渡す形になっているのもいいですね!
公式APIの統合
LazyCell と LazyLock はRustの標準ライブラリの一部として提供されるため、当然追加のクレートを追加する必要がありません。依存は少なければ少ないほどよい。
パフォーマンスの向上
LazyCell
と LazyLock
は、once_cell
クレートを基にした最適化された実装を持ち、より効率的な初期化が可能です。
まとめ
Rustにおける遅延初期化の進化は、lazy_static
クレートの登場から始まり、once_cell
クレートの採用拡大を経て、LazyCell
と LazyLock
の標準ライブラリへの統合に至りました。これにより、開発者はよりシンプルで効率的な遅延初期化を利用できるようになり、コードの可読性や保守性、パフォーマンスが大幅に向上しました。
Rustのエコシステムは、これらの進化により一層強化され、開発者にとって魅力的な選択肢となっています。今後もRustの発展に期待しながら、LazyCell
と LazyLock
を活用して、より良いソフトウェアを開発していきましょう。