diff --git a/blog/content/edition-2/posts/10-heap-allocation/index.ja.md b/blog/content/edition-2/posts/10-heap-allocation/index.ja.md index 5f661d8e..e4e86bcf 100644 --- a/blog/content/edition-2/posts/10-heap-allocation/index.ja.md +++ b/blog/content/edition-2/posts/10-heap-allocation/index.ja.md @@ -23,26 +23,26 @@ translators = ["woodyZootopia"] -## ローカル (局所) 変数とスタティック (静的) 変数 +## 局所 (ローカル) 変数と静的 (スタティック) 変数 -私たちのカーネルでは現在二種類の変数が使用されています:ローカル変数と`static`変数です。ローカル変数は[コールスタック][call stack]に格納されており、変数の定義された関数がリターンするまでの間のみ有効です。スタティック変数はメモリ上の固定された場所に格納されており、プログラムのライフタイム全体で常に生存しています。 +私たちのカーネルでは現在二種類の変数が使用されています:局所変数と`static`変数です。局所変数は[コールスタック][call stack]に格納されており、変数の定義された関数がリターンするまでの間のみ有効です。静的変数はメモリ上の固定された場所に格納されており、プログラムのライフタイム全体で常に生存しています。 -### ローカル変数 +### 局所変数 -ローカル変数は[コールスタック][call stack]に格納されています。これは`プッシュ`と`ポップ`という命令をサポートする[スタックというデータ構造][stack data structure]です。関数に入るたびに、パラメータ、リターンアドレス、呼び出された関数のローカル変数がコンパイラによってプッシュされます: +局所変数は[コールスタック][call stack]に格納されています。これはプッシュ (`push`) とポップ (`pop`) という命令をサポートする[スタックというデータ構造][stack data structure]です。関数に入るたびに、パラメータ、リターンアドレス、呼び出された関数の局所変数がコンパイラによってプッシュされます: [call stack]: https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%BC%E3%83%AB%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF [stack data structure]: https://ja.wikipedia.org/wiki/%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF -![An outer() and an inner(i: usize) function. Both have some local variables. Outer calls inner(1). The call stack contains the following slots: the local variables of outer, then the argument `i = 1`, then the return address, then the local variables of inner.](call-stack.svg) +![outer()とinner(i: usize)関数。両方が局所変数を持っています。outerはinner(1)を呼びます。コールスタックには順に以下の領域があります:outerの局所変数、引数i=1、リターンアドレス、そしてinnerの局所変数。](call-stack.svg) -上の例は、`outer`関数が`inner`関数を呼び出した後のコールスタックを示しています。コールスタックは`outer`のローカル変数を先に持っていることが分かります。`inner`を呼び出すと、パラメータ`1`とこの関数のリターンアドレスがプッシュされます。そこで制御は`inner`へと移り、`inner`は自身のローカル変数をプッシュします。 +上の例は、`outer`関数が`inner`関数を呼び出した後のコールスタックを示しています。コールスタックは`outer`の局所変数を先に持っていることが分かります。`inner`を呼び出すと、パラメータ`1`とこの関数のリターンアドレスがプッシュされます。そこで制御は`inner`へと移り、`inner`は自身の局所変数をプッシュします。 -`inner`関数がリターンすると、コールスタックのこの関数に対応する部分がポップされ、`outer`のローカル変数のみが残ります: +`inner`関数がリターンすると、コールスタックのこの関数に対応する部分がポップされ、`outer`の局所変数のみが残ります: -![The call stack containing only the local variables of outer](call-stack-return.svg) +![outerの局所変数しか持っていないコールスタック](call-stack-return.svg) -`inner`関数のローカル変数はリターンまでしか生存していないことが分かります。Rustコンパイラはこの生存期間 (ライフタイム) を強制し、私たちが値を長く使いすぎてしまうとエラーを投げます。例えば、ローカル変数への参照を返そうとしたときがそうです: +`inner`関数の局所変数はリターンまでしか生存していないことが分かります。Rustコンパイラはこの生存期間 (ライフタイム) を強制し、私たちが値を長く使いすぎてしまうとエラーを投げます。例えば、局所変数への参照を返そうとしたときがそうです: ```rust fn inner(i: usize) -> &'static u32 { @@ -53,24 +53,24 @@ fn inner(i: usize) -> &'static u32 { ([この例をplaygroundで実行する](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6186a0f3a54f468e1de8894996d12819)) -上の例の場合、参照を返すことには意味がありませんが、変数に関数よりも長く生存して欲しいというケースは存在します。すでに私たちのカーネルでそのようなケースに遭遇しています:それは[割り込み記述子表 (IDT) を読み込][load an interrupt descriptor table]もうとしたときで、ライフタイムを延ばすために`static`変数を使う必要があったのでした。 +上の例の場合、参照を返すことには意味がありませんが、変数に関数よりも長く生存して欲しいというケースは存在します。すでに私たちのカーネルでそのようなケースに遭遇しています。それは[割り込み記述子表 (IDT) を読み込][load an interrupt descriptor table]もうとしたときで、ライフタイムを延ばすために`static`変数を使う必要がありました。 [load an interrupt descriptor table]: @/edition-2/posts/05-cpu-exceptions/index.ja.md#idtwodu-miip-mu -### スタティック変数 +### 静的変数 -スタティック変数は、スタックとは別の固定されたメモリ位置に格納されます。このメモリ位置はコンパイル時にリンカによって指定され、実行可能ファイルにエンコードされています。スタティック変数はプログラムの実行中ずっと生存するため、`'static`ライフタイムを持っており、ローカル変数によっていつでも参照することができます。 +静的変数は、スタックとは別の固定されたメモリ位置に格納されます。このメモリ位置はコンパイル時にリンカによって指定され、実行可能ファイルにエンコードされています。静的変数はプログラムの実行中ずっと生存するため、`'static`ライフタイムを持っており、局所変数によっていつでも参照することができます。 -![The same outer/inner example with the difference that inner has a `static Z: [u32; 3] = [1,2,3];` and returns a `&Z[i]` reference](call-stack-static.svg) +![同じouter/innerの例ですが、innerが`static Z: [u32; 3] = [1,2,3];`を持っており、参照`&Z[i]`を返します](call-stack-static.svg) -上の例で`inner`関数がリターンするとき、それに対応するコールスタックは破棄されます。(しかし)スタティック変数は絶対に破棄されない別のメモリ領域にあるため、参照`&Z[1]`はリターン後も有効です。 +上の例で`inner`関数がリターンするとき、それに対応するコールスタックは破棄されます。(しかし)静的変数は絶対に破棄されない別のメモリ領域にあるため、参照`&Z[1]`はリターン後も有効です。 -`'static`ライフタイムの他にもスタティック変数には利点があります:位置がコンパイル時に分かるため、アクセスするために参照が必要ないのです。この特性を私たちの`println`マクロを作る際に利用しました:[スタティックな`Writer`][static `Writer`]をその内部で使うことで、マクロを呼び出す際に`&mut Writer`参照が必要でなくなります。これは他の変数にアクセスできない[例外処理関数][exception handlers]においてとても有用です。 +`'static`ライフタイムの他にも静的変数には利点があります:位置がコンパイル時に分かるため、アクセスするために参照が必要ないのです。この特性を私たちの`println`マクロを作る際に利用しました:[静的な`Writer`][static `Writer`]をその内部で使うことで、マクロを呼び出す際に`&mut Writer`参照が必要でなくなります。これは他の変数にアクセスできない[例外処理関数][exception handlers]においてとても有用です。 [static `Writer`]: @/edition-2/posts/03-vga-text-buffer/index.ja.md#da-yu-de-global-naintahuesu [exception handlers]: @/edition-2/posts/05-cpu-exceptions/index.ja.md#shi-zhuang -しかし、スタティック変数のこの特性には重大な欠点がついてきます:デフォルトでは読み込み専用なのです。Rustがこのルールを強制するのは、例えば二つのスレッドがあるスタティック変数を同時に変更した場合[データ競合][data race]が発生するためです。スタティック変数を変更する唯一の方法は、それを[`Mutex`]型にカプセル化し、あらゆる時刻において`&mut`参照が一つしか存在しないことを保証することです。`Mutex`はすでに[VGAバッファへのスタティックな`Writer`][vga mutex]を作ったときに使いました。 +しかし、静的変数のこの特性には重大な欠点がついてきます:デフォルトでは読み込み専用なのです。Rustがこのルールを強制するのは、例えば二つのスレッドがある静的変数を同時に変更した場合[データ競合][data race]が発生するためです。静的変数を変更する唯一の方法は、それを[`Mutex`]型にカプセル化し、あらゆる時刻において`&mut`参照が一つしか存在しないことを保証することです。`Mutex`は[VGAバッファへの静的な`Writer`][vga mutex]を作ったときにすでに使いました。 [data race]: https://doc.rust-jp.rs/rust-nomicon-ja/races.html [`Mutex`]: https://docs.rs/spin/0.5.2/spin/struct.Mutex.html @@ -78,12 +78,12 @@ fn inner(i: usize) -> &'static u32 { ## 動的 (ダイナミック) メモリ -ローカル変数とスタティック変数を組み合わせれば、それら自体とても強力であり、殆どのユースケースを満足します。しかし、どちらにも制限が存在することも見てきました: +局所変数と静的変数を組み合わせれば、それら自体とても強力であり、ほとんどのユースケースを満足します。しかし、どちらにも制限が存在することも見てきました: -- ローカル変数はそれを定義する関数やブロックが終わるまでしか生存しません。なぜなら、これらはコールスタックに存在し、関数がリターンした段階で破棄されるからです。 -- スタティック変数はプログラムの実行中常に生存するため、必要なくなったときでもメモリを取り戻したり再利用したりする方法がありません。また、所有権のセマンティクスが不明瞭であり、すべての関数からアクセスできてしまうため、変更しようと思ったときには[`Mutex`]で保護してやらないといけません。 +- 局所変数はそれを定義する関数やブロックが終わるまでしか生存しません。なぜなら、これらはコールスタックに存在し、関数がリターンした段階で破棄されるからです。 +- 静的変数はプログラムの実行中常に生存するため、必要なくなったときでもメモリを取り戻したり再利用したりする方法がありません。また、所有権のセマンティクスが不明瞭であり、すべての関数からアクセスできてしまうため、変更しようと思ったときには[`Mutex`]で保護してやらないといけません。 -ローカル・スタティック変数の制約としてもう一つ、固定サイズであることが挙げられます。従ってこれらは要素が追加されたときに動的に大きくなるコレクションを格納することができません(Rustにおいて動的サイズのローカル変数を可能にする[unsized rvalues]の提案が行われていますが、これはいくつかの特定のケースでしかうまく動きません)。 +局所変数・静的変数の制約としてもう一つ、固定サイズであることが挙げられます。従ってこれらは要素が追加されたときに動的に大きくなるコレクションを格納することができません(Rustにおいて動的サイズの局所変数を可能にする[unsized rvalues]の提案が行われていますが、これはいくつかの特定のケースでしかうまく動きません)。 [unsized rvalues]: https://github.com/rust-lang/rust/issues/48055 @@ -91,9 +91,9 @@ fn inner(i: usize) -> &'static u32 { 例を使って見てみましょう: -![The inner function calls `allocate(size_of([u32; 3]))`, writes `z.write([1,2,3]);`, and returns `(z as *mut u32).offset(i)`. The outer function does `deallocate(y, size_of(u32))` on the returned value `y`.](call-stack-heap.svg) +![inner関数は`allocate(size_of([u32; 3]))`を呼び、`z.write([1,2,3]);`で書き込みを行い、`(z as *mut u32).offset(i)`を返します。outer関数は返された値`y`に対して`deallocate(y, size_of(u32))`を行います。](call-stack-heap.svg) -ここで`inner`関数は`z`を格納するためにスタティック変数ではなくヒープメモリを使っています。まず要求されたサイズのメモリブロックを割り当て、`*mut u32`の[生ポインタ][raw pointer]を受け取ります。その後で[`ptr::write`]メソッドを使ってこれに配列`[1,2,3]`を書き込みます。最後のステップとして、[`offset`]関数を使って`i`番目の要素へのポインタを計算しそれを返します(簡単のため、必要なキャストやunsafeブロックをいくつか省略しました)。 +ここで`inner`関数は`z`を格納するために静的変数ではなくヒープメモリを使っています。まず要求されたサイズのメモリブロックを割り当て、`*mut u32`の[生ポインタ][raw pointer]を受け取ります。その後で[`ptr::write`]メソッドを使ってこれに配列`[1,2,3]`を書き込みます。最後のステップとして、[`offset`]関数を使って`i`番目の要素へのポインタを計算しそれを返します(簡単のため、必要なキャストやunsafeブロックをいくつか省略しました)。 [raw pointer]: https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html#%E7%94%9F%E3%83%9D%E3%82%A4%E3%83%B3%E3%82%BF%E3%82%92%E5%8F%82%E7%85%A7%E5%A4%96%E3%81%97%E3%81%99%E3%82%8B [`ptr::write`]: https://doc.rust-lang.org/core/ptr/fn.write.html @@ -101,9 +101,9 @@ fn inner(i: usize) -> &'static u32 { 割り当てられたメモリは`deallocate`の呼び出しによって明示的に解放されるまで生存します。したがって、返されたポインタは、`inner`がリターンしコールスタックの対応する部分が破棄された後も有効です。スタティックメモリと比較したときのヒープメモリの長所は、解放(`outer`内の`deallocate`呼び出しでまさにこれを行っています)後に再利用できるということです。この呼び出しの後、状況は以下のようになります。 -![The call stack contains the local variables of outer, the heap contains z[0] and z[2], but no longer z[1].](call-stack-heap-freed.svg) +![コールスタックはouterの局所変数を持っており、ヒープはz[0]とz[2]を持っているが、z[1]はもう持っていない。](call-stack-heap-freed.svg) -`z[1]`スロットが解放され、次の`allocate`呼び出しで再利用できることが分かります。しかし、`z[0]`と`z[2]`は永久にdeallocateされず、したがって永久に解放されないことも分かります。このようなバグは**メモリリーク**と呼ばれており、しばしばプログラムの過剰なメモリ消費を引き起こします(`inner`をループで何度も呼び出したらどんなことになるか、想像してみてください)。これ自体良くないことに思われるかもしれませんが、実は動的割り当てでは遙かに危険性の高いバグも発生しうるのです。 +`z[1]`スロットが解放され、次の`allocate`呼び出しで再利用できることが分かります。しかし、`z[0]`と`z[2]`は永久にdeallocateされず、したがって永久に解放されないことも分かります。このようなバグは**メモリリーク**と呼ばれており、しばしばプログラムの過剰なメモリ消費を引き起こします(`inner`をループで何度も呼び出したらどんなことになるか、想像してみてください)。これ自体良くないことに思われるかもしれませんが、動的割り当てはもっと危険性の高いバグを発生させうるのです。 ### よくあるミス @@ -183,7 +183,7 @@ Rustの所有権システムはさらに突き詰められており、use-after- Rustにおける動的メモリ割り当ての基礎を学んだわけですが、これをいつ使えば良いのでしょうか?私たちのカーネルは動的メモリ割り当てなしにこれだけやってこられたのに、どうして今になってこれが必要なのでしょうか? -まず覚えておいて欲しいのは、割り当てを行うたびにヒープから空いているスロットを探してこないといけないので、動的メモリ割り当てには少しだけ性能オーバーヘッドがあるということです。このため、特に性能が重要となるカーネルのプログラムにおいては、一般にローカル変数の方が好ましいです。しかし、動的メモリ割り当てが最良の選択肢であるようなケースも存在するのです。 +まず覚えておいて欲しいのは、割り当てを行うたびにヒープから空いているスロットを探してこないといけないので、動的メモリ割り当てには少しだけ性能オーバーヘッドがあるということです。このため、特に性能が重要となるカーネルのプログラムにおいては、一般に局所変数の方が好ましいです。しかし、動的メモリ割り当てが最良の選択肢であるようなケースも存在するのです。 基本的なルールとして、動的メモリは動的なライフタイムや可変サイズを持つような変数に必要とされます。動的なライフタイムを持つ最も重要な型は[**`Rc`**]で、これはラップされた値に対する参照を数えておき、すべての参照がスコープから外れたらそれをdeallocateするというものです。可変サイズを持つ型の例には、[**`Vec`**]、[**`String`**]、その他の[コレクション型][collection types]といった、要素が追加されたときに動的に大きくなるような型が挙げられます。これらの型は、容量が一杯になると、より大きい量のメモリを割り当て、すべての要素をコピーし、古い割り当てをdeallocateすることにより対処します。 @@ -232,7 +232,7 @@ build-std = ["core", "compiler_builtins", "alloc"] error: no global memory allocator found but one is required; link to std or add #[global_allocator] to a static item that implements the GlobalAlloc trait. (エラー:グローバルメモリアロケータが見つかりませんが、一つ必要です。 - stdをリンクするか、GlobalAllocトレイトを実装するスタティックな要素に#[global_allocator]を付けてください。) + stdをリンクするか、GlobalAllocトレイトを実装する静的な要素に#[global_allocator]を付けてください。) error: `#[alloc_error_handler]` function required, but not found (エラー:`#[alloc_error_handler]`関数が必要ですが、見つかりません) @@ -393,7 +393,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { 上のコードを実行すると、`alloc_error_handler`関数が呼ばれるのが分かります: -![QEMU printing "panicked at `allocation error: Layout { size_: 4, align_: 4 }, src/lib.rs:89:5"](qemu-dummy-output.png) +![QEMUが"panicked at `allocation error: Layout { size_: 4, align_: 4 }, src/lib.rs:89:5"と出力している。](qemu-dummy-output.png) `Box::new`関数は暗黙のうちにグローバルアロケータの`alloc`関数を呼び出すため、エラーハンドラが呼ばれました。私たちのダミーアロケータは常にヌルポインタを返すので、あらゆる割り当てが失敗するのです。これを修正するためには、使用可能なメモリを実際に返すアロケータを作る必要があります。 @@ -630,12 +630,12 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { 実行すると、以下のような結果を得ます: -![QEMU printing ` +![QEMUが` heap_value at 0x444444440000 vec at 0x4444444408000 current reference count is 2 reference count is 1 now -](qemu-alloc-showcase.png) +`と出力している](qemu-alloc-showcase.png) ポインタが`0x_4444_4444_*`で始まることから、`Box`と`Vec`の値は想定通りヒープ上にあることが分かります。参照カウントされた値も期待したとおり振る舞っており、`clone`呼び出しの後では参照カウントは2になり、インスタンスの一方がドロップされた後では再び1になっています。 @@ -646,7 +646,7 @@ reference count is 1 now もちろん`alloc`クレートにはもっと多くのアロケーション・コレクション型があり、今やそれらのすべてを私たちのカーネルで使うことができます。それには以下が含まれます: - スレッドセーフな参照カウントポインタ[`Arc`] -- 所有された文字列型[`String`]と[`format!`]マクロ +- 文字列を所有する型[`String`]と[`format!`]マクロ - [`LinkedList`] - 必要に応じてサイズを大きくできるリングバッファ[`VecDeque`] - プライオリティキューである[`BinaryHeap`] @@ -803,4 +803,4 @@ many_boxes... [ok] ## 次は? -この記事でヒープ割り当て機能のサポートを追加したとはいえ、ほとんどの仕事は`linked_list_allocator`クレートに任せてしまっています。次の記事では、アロケータをゼロから実装する方法を詳細にお伝えします。可能なアロケータの設計を複数提示し、それらを単純化したものを実装する方法を示し、それらの利点と欠点を説明します。 +この記事ではヒープ割り当て機能のサポートを追加しましたが、ほとんどの仕事は`linked_list_allocator`クレートに任せてしまっています。次の記事では、アロケータをゼロから実装する方法を詳細にお伝えします。可能なアロケータの設計を複数提示し、それらを単純化したものを実装する方法を示し、それらの利点と欠点を説明します。