HN 日本語サマリー

← 一覧へ戻る
プログラミング

インプレース初期化の4つのレベル(Rust)

Four Levels of In-Place Initialization (Rust) (blog.yoshuawuyts.com)

5 pointsby dabinat0 コメント

要約

Rustにおけるインプレース初期化は、型をメモリ上の特定の位置に、追加の移動やコピーなしで直接構築することを可能にする技術です。これにより、大きな型の効率化やスタックオーバーフローの防止が期待できます。しかし、一部の型はアドレスに依存するため、移動ができないという制約があります。著者は、このインプレース初期化を言語でどのようにエンコードするかについて、4段階の機能階層を提案しています。これは、低レベルの生ポインタから、コンパイラがチェック可能な参照、エルゴノミクスを重視した配置関数、そして最終的には自動的なムーブ除去へと発展していく考え方です。

全文翻訳

インプレース初期化の目標は、追加の移動やコピーなしで、型をメモリ上の特定の位置に直接構築できるようにすることです。 大きな型を扱う場合、これはより効率的になり、スタックオーバーフローを防ぐことさえできます。 しかし、一部の型はアドレスに依存するため、正当性の理由から移動できません。 インプレース初期化を言語でどのようにエンコードするかについては、意見が分かれています。 相反する要件と制約が関与しており、それらを調和させるのは難しいです。 この問題空間に取り組む正しい方法は、単一の機能を紹介することではなく、インプレース初期化のための4段階の機能階層を導入することだと私は信じています。 レベル0:生ポインタ 最も低いレベルでは、生ポインタとMaybeUninitがあります。 これは、配置をエンコードする最も柔軟な方法ですが、他のすべてをほぼ犠牲にします。 これは、pin-initクレートとplacementクレートが内部的に実装されている方法です。 これについてさらに読むには、完全なデシュアリングを検討した私のplacement関数に関する投稿を参照してください。 このレベルを私が分類する方法は、「何もないよりはまし」です。 エコシステムに配置をエンコードする方法があるのは良いことですが、それは望まれるすべてを残しています。 初期化を延期するために、MaybeUninit、生ポインタ、unsafeを使用した基本的な例を次に示します。 rust use std::mem::MaybeUninit; let mut x = MaybeUninit::<A>::uninit(); // 1. 型`A`の初期化されていない場所`x`を作成します let y: *mut A = x.as_mut_ptr(); // 2. `x`への生ポインタ`y`を取得します unsafe { y.write(A { .. }) }; // 3. `y`を通じてすべてのフィールドを初期化します let mut x = unsafe { x.assume_init() }; // 5. `x`を初期化済みとして記録します let y: &mut A = &mut x; // 6. `x`は初期化済みであり、通常どおり使用できます コピー ステップ5で`x`の値を移動します。 移動せずに`x`を初期化済みとして記録したい場合は、MaybeUninit::assume_init_mutを呼び出す必要がありますが、これは`T`をインプレースで変更するのではなく`&mut T`を返します。 追加の言語機能なしでは、値を移動したり参照に変換したりせずに、所有された値を初期化済みとして記録することは不可能です。 レベル1:参照 生ポインタは非常に強力ですが、コンパイラはそれらの正しさをチェックできないため、プログラマに追加の負担がかかります。 生ポインタができることのほとんどをエンコードできる抽象化が必要ですが、コンパイラが合理的な時間内に静的にチェックできる方法でエンコードする必要があります。 これに対する私の好ましい提案は、Ding Xiang Feiの`&uninit` / `&own`参照ペアですが、このスロットを埋めることができる他の提案もあります。 `&uninit`/`&own`の考え方は、型への`&uninit`参照を取得でき、すべてのフィールドが初期化されたら、`&own`参照に記録できるということです。 ここには4つのステップがあります。 型`A`の初期化されていない場所`x`を作成します。 `x`への`&uninit`参照を取得します。 値を初期化し、`&own`参照を返します。 代入によって初期化を記録します。 rust let x: A; let y: &uninit A = &x; // 2. `x`への`&uninit`参照`y`を取得します *y = A { .. }; // 3. `y`のすべてのフィールドを初期化します let y: &own A = y; // 4. ここから参照`y`は`&own`になります x = y; // 5. `x`を初期化済みとして記録します let y: &mut A = &mut x; // 6. `x`は初期化済みになり、通常どおり使用できます コピー この提案の主な革新は、初期化されていない場所を、私たちが言及し参照できるファーストクラスのオブジェクトにすることです。 上記の例は、`let a; a = A { ... };`と書くことで、`&uninit`と`&own`なしで今日すでに書くことができます。 しかし、これは関数をまたぐことはできません。これは`&uninit`/`&own`でできることです。 rust // `&uninit A`を`&own A`に変換します。 fn init_a<'a>(y: &'a uninit A) -> &'a own A { *y = A { .. }; y } let x: A; // 1. 型`A`の初期化されていない場所`x`を作成します x = init_a(&x); // 2. `x`を初期化します let y: &mut A = &mut x; // 3. `x`は初期化済みになり、通常どおり使用できます コピー これは簡単な機能ではありませんが、簡単な問題でもありません。 これにより、初期化されていない値はファーストクラスになり、安全に渡して初期化できるようになります。 設計上、可能な限り表現力豊かであることを目指しており、何よりも制御を優先します。 レベル2:配置関数 参照が制御を優先するのに対し、配置関数はエルゴノミクスを優先します。 配置関数は、戻り値のキーワードをコピーではなくアウトポインタにデータを書き込むように書き換える関数です。 これは、生ポインタまたは`&uninit`/`&own`参照のいずれかを使用して実装できます。 しかし、これらの機能のいずれとも異なり、関数シグネチャへのさらなる変更は必要ありません。 これが役立つところを示すために、既存のコードを配置に移行する方法を考える必要があります。 ここでは、型`A`の値を返し、それを変数`x`に代入する典型的な関数を示します。 rust // 型`A`の値を作成します fn init_a() -> A { A { .. } } let x = init_a(); // 1. 型`A`の値を作成します コピー `&uninit`と`&own`を使用したインプレース初期化の例と比較すると、これがどれほど単純かがわかります。 派手な参照、ライフタイム、記録はありません。 しかし残念ながら、それはコピーも伴います。もし`A`が多くのフィールドを含んでいる場合、問題になる可能性があります。 したがって、理想的には、すべての儀式なしで配置できるものが必要です。 rust // 型`A`をインプレースで作成します #[emplace] fn init_a() -> A { A { .. } } let x = init_a(); // 1. 型`A`の値をインプレースで作成します コピー 悪くないですよね? もちろん、これは`&uninit`+`&own`ほど柔軟ではありません。 しかし、一般的なケースではこれで十分なはずです。 ただし、これは単発の属性ではなく、独自のキーワードにしたいと考えています。 私の現在の考えでは、これを`const`のような効果としてエンコードし、すべての効果を`with`キーワードを使用して公開することです。 rust // 型`A`をインプレースで作成します fn init_a() -> A with emplace { A { .. } } let x = init_a(); // 1. 型`A`の値をインプレースで作成します コピー `emplace`属性が付いた関数は、値をコピーするのではなくアウトポインタに書き込むことを保証します。 これにより、これらの関数は`!Move`型を「返す」ことができ、これは無条件に自己参照型(unconditionally self-referential types)の安全なコンストラクタの要件です。 レベル3:自動ムーブ除去 RFC 3943では、AmanieuはMIR(Mid-level Intermediate Representation)のムーブ除去の追加を提案しています。 これにより、コンパイラは最適化としてMIRレベルで自動的にムーブを削除できるようになり、以前に検討した例の一部に適用される可能性があります。 rust // 型`A`の値を作成します fn init_a() -> A { A { .. } // `A`が2kBのサイズであると仮定します } let x = init_a(); // 1. オプティマイザは`x`がインプレースで作成されることを保証します。 コピー これは「配置関数」の提案と非常によく似ていますが、最適化としてエンコードされています。 最適化はプログラムのパフォーマンスにのみ影響し、セマンティクスには影響しないため、依存することはできません。 たとえば、MIRムーブ除去された関数から`!Move`型を返すことは依然として許可されないことを意味します。なぜなら、除去はコンパイラの内部実装の詳細であり、言語の一部ではないからです。 もし特定の式の振る舞いが言語の正当性にとって重要である場合、それは表記法で表面化されるべきです。 たとえ特定の式が常に配置されることを保証できたとしても、例外が存在する限り、やがて私たちはlvalue、rvalue、prvalue、glvalue、xvalueの違いを説明することになるでしょう。 私は、プログラマが文脈の手がかりから推測することを期待するよりも、コードで要件を書き出す方がはるかに良いと考えています。 おそらく、Rustがすべての場所にあるすべての式が配置を保証できる未来があるでしょう。 その時点で、配置にオプトインするための表記法は冗長になり、エディション全体で式のデフォルトの動作にすることを選択するかもしれません。 しかし、一度に単一の機能でこれを行うことはできないため、2つの補完的な機能から始める方が良いでしょう。これは、`const`で行ったことと似ています。 1これはC++を参照しており、これらの用語はすべて意味を持ち(私の