Refine the translation of post 11

This commit is contained in:
woodyZootopia
2023-11-26 13:22:08 +09:00
parent 53d181d57b
commit 9b1791a48d

View File

@@ -12,11 +12,11 @@ translation_based_on_commit = "2e3230eca2275226ec33c2dfe7f98f2f4b9a48b4"
translators = ["woodyZootopia"] translators = ["woodyZootopia"]
+++ +++
この記事ではヒープアロケータをゼロから実装する方法を説明します。バンプアロケータ、連結リストアロケータ、固定サイズブロックアロケータのような様々なアロケータの設計を示し、それらについて議論します。3つそれぞれのデザインについて、私たちのカーネルに使える基礎的な実装を作ります。 この記事ではヒープアロケータをゼロから実装する方法を説明します。バンプアロケータ、連結リストアロケータ、固定サイズブロックアロケータなどの様々なアロケータの設計を示し、それらについて議論します。3つそれぞれのデザインについて、私たちのカーネルに使える基礎的な実装を作ります。
<!-- more --> <!-- more -->
このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-11` ブランチ][post branch]にあります。 このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事のソースコード全体は[`post-11` ブランチ][post branch]にあります。
[GitHub]: https://github.com/phil-opp/blog_os [GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments [at the bottom]: #comments
@@ -37,9 +37,9 @@ translators = ["woodyZootopia"]
### 設計目標 ### 設計目標
アロケータの責任は、利用可能なヒープメモリを管理することです。`alloc`が呼ばれたら未使用のメモリを返し、`dealloc`によって解放されたメモリが再利用できるように記録をとる必要があります。最も重要なことは、すでに他の場所で使用されているメモリを決して渡してはならないということでこれをすると未定義動作が起きてしまいます。 アロケータの責任は、利用可能なヒープメモリを管理することです。`alloc`が呼ばれたら未使用のメモリを返し、`dealloc`によって解放されたメモリが再利用できるように記録をとる必要があります。最も重要なことは、すでに他の場所で使用されているメモリを決して渡してはならないということです。これをすると未定義動作が起きてしまいます。
正確さのほかにも多くの二次的な設計目標があります。たとえば、アロケータは利用可能なメモリを効果的に利用し、[**断片化**][_fragmentation_]があまり起きないようにすべきです。さらに、並列アプリケーションにもうまく機能し、任意の数のプロセッサに拡張できなくてはなりません。性能を最大化するため、CPUキャッシュに合わせてメモリレイアウトを最適化し[キャッシュの局所性][cache locality]を改善したり[false sharing]を回避するなどしても良いかもしれません。 メモリの正しい管理のほかにも多くの二次的な設計目標があります。たとえば、アロケータは利用可能なメモリを効果的に利用し、[**断片化**][_fragmentation_]があまり起きないようにすべきです。さらに、並列アプリケーションにもうまく機能し、任意の数のプロセッサに拡張できなくてはなりません。性能を最大化するため、CPUキャッシュに合わせてメモリレイアウトを最適化し[キャッシュの局所性][cache locality]を改善したり[false sharing]を回避することすらするかもしれません。
[cache locality]: https://www.geeksforgeeks.org/locality-of-reference-and-cache-operation-in-cache-memory/ [cache locality]: https://www.geeksforgeeks.org/locality-of-reference-and-cache-operation-in-cache-memory/
[_fragmentation_]: https://en.wikipedia.org/wiki/Fragmentation_(computing) [_fragmentation_]: https://en.wikipedia.org/wiki/Fragmentation_(computing)
@@ -53,20 +53,20 @@ translators = ["woodyZootopia"]
## バンプアロケータ ## バンプアロケータ
最も単純なアロケータの設計は**バンプアロケータ****スタックアロケータ**とも呼ばれる)です。メモリを直線的に割り当て、割り当てられたバイト数と割り当ての数のみを管理します。このアロケータは非常に特定のユースケースでのみ有用です——なぜなら、一度にすべてのメモリを解放することしかできないという厳しい制約があるからです。 最も単純なアロケータの設計は**バンプアロケータ****スタックアロケータ**とも呼ばれる)です。メモリを直線的に割り当て、割り当てられたバイト数と割り当ての数のみを管理します。このアロケータは非常に特定のユースケースでのみ有用です──なぜなら、一度にすべてのメモリを解放することしかできないという厳しい制約があるからです。
### 考え方 ### 考え方
バンプアロケータの考え方は、未使用のメモリの開始位置を指す`next`変数を増やす("bump" する)ことによって、メモリを順に割り当てるというものです。はじめ、`next`はヒープの開始アドレスに等しいです。`next`が使用済みメモリと未使用メモリの境界を常に指すよう、この値は各割り当てにおいて割り当てサイズだけ増加します。 バンプアロケータの考え方は、未使用のメモリの開始位置を指す`next`変数を増やす("bump" する)ことによって、メモリを順に割り当てるというものです。はじめ、`next`はヒープの開始アドレスに等しいです。`next`は、各割り当てにおいて割り当てサイズだけ増加し、この値が使用済みメモリと未使用メモリの境界を常に指すようにします。
![3つの時点におけるヒープメモリ領域 ![3つの時点におけるヒープメモリ領域
1: ヒープの開始地点に一つの割り当てが存在する。`next`ポインタはその終端を指している。 1: ヒープの開始地点に一つの割り当てが存在する。`next`ポインタはその終端を指している。
2: 二つ目の割り当てが一つ目のすぐ右に追加された。`next`ポインタは二つ目の割り当ての終端を指している。 2: 二つ目の割り当てが一つ目のすぐ右に追加された。`next`ポインタは二つ目の割り当ての終端を指している。
3: 三つ目の割り当てが二つ目のすぐ右に追加された。`next`ポインタは三つ目の割り当ての終端を指している。](bump-allocation.svg) 3: 三つ目の割り当てが二つ目のすぐ右に追加された。`next`ポインタは三つ目の割り当ての終端を指している。](bump-allocation.svg)
`next`ポインタは1つの方向にしか移動しないため、同じメモリ領域を2回渡すことはありません。ヒープの終わりに達すると、れ以上のメモリを割り当てることができないので、次の割り当てでメモリ不足エラーが発生します。 `next`ポインタは1つの方向にしか移動しないため、同じメモリ領域を2回渡すことはありません。これがヒープの終わりに達すると、れ以上のメモリを割り当てることができないので、次の割り当てでメモリ不足エラーが発生します。
多くの場合、バンプアロケータは「割り当てカウンタ」付きで実装されます。これは、`alloc`の呼び出しのたび1増加し、`dealloc`の呼び出しのたび1減少します。割り当てカウンタがゼロになることは、ヒープ上のすべての割り当てがdeallocateされたことを意味します。このとき、`next`ポインタをヒープの開始アドレスにリセットし、ヒープメモリ全体を再び割り当てに使えるようにすることができます。 多くの場合、バンプアロケータは「割り当てカウンタ」付きで実装されます。これは、`alloc`の呼び出しのたび1増加し、`dealloc`の呼び出しのたび1減少します。割り当てカウンタがゼロになることは、ヒープ上のすべての割り当てが解除されたことを意味します。このとき、`next`ポインタをヒープの開始アドレスにリセットし、ヒープメモリ全体を再び割り当てに使えるようにすることができます。
### 実装 ### 実装
@@ -113,13 +113,13 @@ impl BumpAllocator {
} }
``` ```
`heap_start`フィールドと`heap_end`フィールドは、ヒープメモリ領域の下限と上限を管理します。呼び出し元は、これらのアドレスが有効であることを保証する必要があります。そうでない場合、アロケータは不正なメモリを返すでしょう。このため、`init`関数の呼び出しは`unsafe`ある必要があります `heap_start`フィールドと`heap_end`フィールドは、ヒープメモリ領域の下限と上限を管理します。呼び出し元は、これらのアドレスが有効であることを保証する必要があります。そうでない場合、アロケータは不正なメモリを返すでしょう。このため、`init`関数の呼び出しは`unsafe`なければなりません
`next`フィールドの目的は、常にヒープの最初の未使用バイト、つまり次の割り当ての開始アドレスを指すことです。最初はヒープ全体が未使用であるため、`init`関数では`heap_start`に設定されています。各割り当てで、このフィールドは割り当てサイズだけ増加("bump"し、同じメモリ領域を2回返さないようにします。 `next`フィールドの目的は、常にヒープの最初の未使用バイト、つまり次の割り当ての開始アドレスを指すことです。最初はヒープ全体が未使用であるため、`init`関数では`heap_start`に設定されています。各割り当てで、このフィールドは割り当てサイズだけ増加("bump"し、同じメモリ領域を2回返さないようにします。
`allocations`フィールドは、有効な割り当てのシンプルなカウンタで、最後の割り当てが解放されたときにアロケータをリセットするためにあります。0で初期化されます。 `allocations`フィールドは、有効な割り当ての単純なカウンタで、最後の割り当てが解放されたときにアロケータをリセットするためにあります。0で初期化ます。
インターフェイスを`linked_list_allocator`クレートによって提供されるアロケータと同じにするために、初期化を`new`関数の中で直接実行するのではなく、別の`init`関数を作成するようにしました。こうすることで、コードの変更なしにアロケータを切り替えることができます。 インターフェイスを`linked_list_allocator`クレートによって提供されるアロケータと同じにするために、初期化を`new`関数の中で直接実行するのではなく、別の`init`関数を作ました。こうすることで、コードの変更なしにアロケータを切り替えることができます。
### `GlobalAlloc`を実装する ### `GlobalAlloc`を実装する
@@ -171,7 +171,7 @@ unsafe impl GlobalAlloc for BumpAllocator {
まず、割り当ての開始アドレスとして`next`フィールドを使用します。次に、割り当ての終端アドレス(ヒープの次の未使用アドレスでもある)を指すように`next`フィールドを更新します。`allocations`カウンタを1増やしてから、割り当ての開始アドレスを`*mut u8`ポインタとして返します。 まず、割り当ての開始アドレスとして`next`フィールドを使用します。次に、割り当ての終端アドレス(ヒープの次の未使用アドレスでもある)を指すように`next`フィールドを更新します。`allocations`カウンタを1増やしてから、割り当ての開始アドレスを`*mut u8`ポインタとして返します。
境界チェックやアラインメント調整を行わないので、この実装はまだsafeではないことに注意してください。まあいずれにせよ、以下のエラーでコンパイルに失敗するのでたいした問題ではないのですが: 境界チェックやアラインメント調整を行わないので、この実装はまだ安全ではないことに注意してください。まあいずれにせよ、以下のエラーでコンパイルに失敗するのでたいした問題ではないのですが:
``` ```
error[E0594]: cannot assign to `self.next` which is behind a `&` reference error[E0594]: cannot assign to `self.next` which is behind a `&` reference
@@ -190,7 +190,7 @@ error[E0594]: cannot assign to `self.next` which is behind a `&` reference
#### `GlobalAlloc`と可変性 #### `GlobalAlloc`と可変性
この可変性の問題にどんな解決策が可能かを見る前に、`GlobalAlloc`トレイトメソッドがなぜ`&self`引数で定義されているのかを考えてみましょう。[前回の記事][global-allocator]で見たように、グローバルヒープアロケータは`GlobalAlloc`トレイトを実装する`static``#[global_allocator]`属性を追加することによって定義されます。<ruby>静的<rp> (</rp><rt>スタティック</rt><rp>) </rp></ruby>変数はRustでは不変であるため、この静的なアロケータで`&mut self`を取るメソッドを呼び出すことはできません。このため`GlobalAlloc`のすべてのメソッドは、不変な`&self`参照のみを取ります。 この可変性の問題にどんな解決策が可能かを見る前に、`GlobalAlloc`トレイトメソッドがなぜ`&self`引数で定義されているのかを考えてみましょう。[前回の記事][global-allocator]で見たように、グローバルヒープアロケータは`GlobalAlloc`トレイトを実装する`static``#[global_allocator]`属性を追加することによって定義されます。<ruby>静的<rp> (</rp><rt>スタティック</rt><rp>) </rp></ruby>変数はRustでは不変であるため、この静的なアロケータで`&mut self`を取るメソッドを呼び出すことはできません。よって`GlobalAlloc`のすべてのメソッドは、不変な`&self`参照のみを取ります。
[global-allocator]: @/edition-2/posts/10-heap-allocation/index.ja.md#global-allocator-shu-xing [global-allocator]: @/edition-2/posts/10-heap-allocation/index.ja.md#global-allocator-shu-xing
@@ -247,11 +247,11 @@ impl<A> Locked<A> {
} }
``` ```
この型は、`spin::Mutex<A>`の<ruby>汎用<rp> (</rp><rt>ジェネリック</rt><rp>) </rp></ruby>ラッパです。ラップされる型`A`に制限はないので、アロケータだけでなく、あらゆる種類の型をラップするために使用できます。このラッパは、指定された値をラップする単純な`new`コンストラクタ関数を提供しています。ラップされた`Mutex``lock`を呼び出す`lock`関数もあると便利なので提供しています。`Locked`型は汎用的で他のアロケータの実装にも役立つため、親の`allocator`モジュールに入れました この型は、`spin::Mutex<A>`の<ruby>汎用<rp> (</rp><rt>ジェネリック</rt><rp>) </rp></ruby>ラッパです。ラップされる型`A`に制限はないので、アロケータだけでなく、あらゆる種類の型をラップするために使用できます。このラッパは、指定された値をラップする単純な`new`コンストラクタ関数を提供しています。ラップされた`Mutex``lock`を呼び出す`lock`関数も便利なので提供しています。`Locked`型はとても汎用的であり、他のアロケータの実装にも役立つため、親の`allocator`モジュールに入れることにします
#### `Locked<BumpAllocator>`の実装 #### `Locked<BumpAllocator>`の実装
`Locked`型は(`spin::Mutex`とは違って)私たち自身のクレートで定義されているため、私たちのバンプアロケータに`GlobalAlloc`型を実装するために使用できます。完全な実装は次のようになります: `Locked`型は(`spin::Mutex`とは違って)私たちクレートの中で定義されているため、私たちのバンプアロケータに`GlobalAlloc`型を実装するために使用できます。実装の全体は次のようになります:
```rust ```rust
// in src/allocator/bump.rs // in src/allocator/bump.rs
@@ -290,11 +290,11 @@ unsafe impl GlobalAlloc for Locked<BumpAllocator> {
} }
``` ```
`alloc``dealloc`は両方、まず、`inner`フィールドを通じて[`Mutex::lock`]メソッドを呼び出し、ラップされたアロケータ型への可変参照を取得します。インスタンスはメソッドの終了までロックされたままであるため、(まもなくスレッドのサポートを追加しますが)マルチスレッドになってもデータ競合が発生することはありません。 `alloc``dealloc`は両方、まず、`inner`フィールドを通じて[`Mutex::lock`]メソッドを呼び出し、ラップされたアロケータ型への可変参照を取得します。インスタンスはメソッドの終了までロックされたままであるため、(まもなくスレッドのサポートを追加するのですが)マルチスレッドになってもデータ競合が発生することはありません。
[`Mutex::lock`]: https://docs.rs/spin/0.5.0/spin/struct.Mutex.html#method.lock [`Mutex::lock`]: https://docs.rs/spin/0.5.0/spin/struct.Mutex.html#method.lock
前のプロトタイプと比較してみると、`alloc`の実装はアラインメント要件を守るようになっており、割り当てがヒープメモリ領域内にあることを保証するために境界チェックを実行するようになりました。この関数はまず、`next`アドレスを`Layout`引数で指定されたアラインメントに切り上げます。`align_up`関数のコードはすぐ後で示します。次に、要求された割り当てサイズを`alloc_start`に足して、割り当ての終端アドレスを得ます。巨大な割り当てが試みられた際に整数のオーバーフローが起きることを防ぐため、[`checked_add`]メソッドを使っています。オーバーフローが発生した場合、または割り当ての終端アドレスがヒープの終端アドレスよりも大きくなる場合、メモリ不足であることを示すためにヌルポインタを返します。それ以外の場合は、以前のように、`next`アドレスを更新し、`allocations`カウンタを1増やします。最後に、`*mut u8`ポインタに変換された`alloc_start`アドレスを返します。 前のプロトタイプと比較してみると、`alloc`の実装はアラインメント要件を守るようになっており、割り当てがヒープメモリ領域内にあることを保証するために境界チェックを実行するようになっています。この関数はまず、`next`アドレスを`Layout`引数で指定されたアラインメントに切り上げます。`align_up`関数のコードはすぐ後で示します。次に、要求された割り当てサイズを`alloc_start`に足して、割り当ての終端アドレスを得ます。巨大な割り当てが試みられた際に整数のオーバーフローが起きることを防ぐため、[`checked_add`]メソッドを使っています。オーバーフローが発生した場合、または割り当ての終端アドレスがヒープの終端アドレスよりも大きくなる場合、メモリ不足であることを示すためにヌルポインタを返します。それ以外の場合は、以前のように、`next`アドレスを更新し、`allocations`カウンタを1増やします。最後に、`*mut u8`ポインタに変換された`alloc_start`アドレスを返します。
[`checked_add`]: https://doc.rust-lang.org/std/primitive.usize.html#method.checked_add [`checked_add`]: https://doc.rust-lang.org/std/primitive.usize.html#method.checked_add
[`Layout`]: https://doc.rust-lang.org/alloc/alloc/struct.Layout.html [`Layout`]: https://doc.rust-lang.org/alloc/alloc/struct.Layout.html
@@ -312,14 +312,14 @@ unsafe impl GlobalAlloc for Locked<BumpAllocator> {
fn align_up(addr: usize, align: usize) -> usize { fn align_up(addr: usize, align: usize) -> usize {
let remainder = addr % align; let remainder = addr % align;
if remainder == 0 { if remainder == 0 {
addr // addr already aligned addr // addr はすでに丸められていた
} else { } else {
addr - remainder + align addr - remainder + align
} }
} }
``` ```
この関数はまず、`align``addr`を割った[余り][remainder]を計算します。余りが`0`の場合、アドレスはすでに指定されたアラインメントに整列されているということです。それ以外の場合は、余りが0になるように余りを引いてアドレスをアラインし、アドレスが元のアドレスよりも小さくならないようにアラインメントを足します。 この関数はまず、`align``addr`を割った[余り][remainder]を計算します。余りが`0`の場合、アドレスはすでに指定されたアラインメントに丸められているということです。それ以外の場合は、余りが0になるように余りを引いてアドレスをアラインし、アドレスが元のアドレスよりも小さくならないようにアラインメントを足します。
[remainder]: https://en.wikipedia.org/wiki/Euclidean_division [remainder]: https://en.wikipedia.org/wiki/Euclidean_division
@@ -339,7 +339,7 @@ fn align_up(addr: usize, align: usize) -> usize {
[`Layout`]: https://doc.rust-lang.org/alloc/alloc/struct.Layout.html [`Layout`]: https://doc.rust-lang.org/alloc/alloc/struct.Layout.html
[bitmask]: https://en.wikipedia.org/wiki/Mask_(computing) [bitmask]: https://en.wikipedia.org/wiki/Mask_(computing)
- `align`は2の累乗であるため、その[2進数表現][binary representation]は1つのビットのみが1であるはずである`0b000100000`)。これは、`align - 1`ではそれより下位のすべてのビットが1であることを意味する`0b00011111`)。 - `align`は2の累乗であるため、その[2進数表現][binary representation]は1つのビットのみが1であるはずである`0b000100000`)。これは、`align - 1`ではそれより下位のすべてのビットが1であることを意味する`0b000011111`)。
- `!`演算子すなわち[ビットごとの`NOT`][bitwise `NOT`]を行うことで、「`align`より下位のビット」以外がすべて1であるような数字を得ることができる`0b…111111111100000` - `!`演算子すなわち[ビットごとの`NOT`][bitwise `NOT`]を行うことで、「`align`より下位のビット」以外がすべて1であるような数字を得ることができる`0b…111111111100000`
- あるアドレスと`!(align - 1)`の間で[ビットごとの`AND`][bitwise `AND`]を行うことで、アドレスを**下向きに**アラインする。なぜなら、`align`よりも小さいビットがすべて0になるからである。 - あるアドレスと`!(align - 1)`の間で[ビットごとの`AND`][bitwise `AND`]を行うことで、アドレスを**下向きに**アラインする。なぜなら、`align`よりも小さいビットがすべて0になるからである。
- 下向きではなく上向きにアラインしたいので、ビットごとの`AND`の前に`addr``align - 1`だけ増やしておく。こうすると、すでにアラインされているアドレスには影響がないが、アラインされていないアドレスは次のアラインメント境界に丸められるようになる。 - 下向きではなく上向きにアラインしたいので、ビットごとの`AND`の前に`addr``align - 1`だけ増やしておく。こうすると、すでにアラインされているアドレスには影響がないが、アラインされていないアドレスは次のアラインメント境界に丸められるようになる。
@@ -383,7 +383,7 @@ many_boxes... [ok]
### 議論 ### 議論
バンプアロケータの大きな利点は、非常に速いことです。`alloc``dealloc`のたびにサイズの合うメモリを動的に探索し様々な管理タスクを行う必要があるほかのアロケータの設計(後述)に比べると、バンプアロケータはたった数個のアセンブリ命令に[最適化することができる][bump downwards]のですから。これによりバンプアロケータは、メモリ割り当ての性能を最大化したいとき、例えば[仮想DOMライブラリ][virtual DOM library]を作成したいときなどに役に立ちます。 バンプアロケータの大きな利点は、非常に速いことです。`alloc``dealloc`のたびにサイズの合うメモリを動的に探索し様々な管理タスクを行う必要があるほかのアロケータの設計(後述)に比べると、バンプアロケータはたった数個のアセンブリ命令に[最適化することができる][bump downwards]のですから。これによりバンプアロケータは、メモリ割り当ての性能を最大化したいとき、例えば[仮想DOMライブラリ][virtual DOM library]を作成したいときなどに役に立ちます。
[bump downwards]: https://fitzgeraldnick.com/2019/11/01/always-bump-downwards.html [bump downwards]: https://fitzgeraldnick.com/2019/11/01/always-bump-downwards.html
[virtual DOM library]: https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ [virtual DOM library]: https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/
@@ -395,7 +395,7 @@ many_boxes... [ok]
#### バンプアロケータの欠点 #### バンプアロケータの欠点
バンプアロケータの主な制約は、すべてのメモリ割り当てが解放されないと<ruby>割り当て解除<rp> (</rp><rt>デアロケート</rt><rp>) </rp></ruby>されたメモリを再利用できないことです。これは、たった一つでも寿命の長い割り当てがあると、メモリの再利用ができなくなってしまうことを意味します。`many_boxes`テストを少し変更したものを追加すると、それを見ることができます。 バンプアロケータの主な制約は、すべてのメモリ割り当てが解放されないと<ruby>割り当て解除<rp> (</rp><rt>デアロケート</rt><rp>) </rp></ruby>されたメモリを再利用できないことです。これは、たった一つでも寿命の長い割り当てがあると、メモリの再利用ができなくなってしまうことを意味します。`many_boxes`テストを少し変更したものを追加すると、それが起こるのを見ることができます。
```rust ```rust
// in tests/heap_allocation.rs // in tests/heap_allocation.rs
@@ -413,7 +413,7 @@ fn many_boxes_long_lived() {
`many_boxes`テストと同様、このテストは大量の割り当てを行うことで、アロケータが解放されたメモリを再利用できていない場合にメモリ不足エラーを引き起こします。さらに、このテストではループの間ずっと存在している`long_lived`という割り当てを追加しています。 `many_boxes`テストと同様、このテストは大量の割り当てを行うことで、アロケータが解放されたメモリを再利用できていない場合にメモリ不足エラーを引き起こします。さらに、このテストではループの間ずっと存在している`long_lived`という割り当てを追加しています。
この新しいテストを実行しようとすると、実際に失敗することがわかります: この新しいテストを実行しようとすると、確かに失敗することがわかります:
``` ```
> cargo test --test heap_allocation > cargo test --test heap_allocation
@@ -426,7 +426,7 @@ many_boxes_long_lived... [failed]
Error: panicked at 'allocation error: Layout { size_: 8, align_: 8 }', src/lib.rs:86:5 Error: panicked at 'allocation error: Layout { size_: 8, align_: 8 }', src/lib.rs:86:5
``` ```
この失敗が発生する理由を詳細に理解してみましょう。まず、ヒープの先頭に変数`long_lived`の割り当てが作成され、`allocations`カウンタが1増加します。ループの反復ごとに、一時的な割り当てが作成され、次の反復が始まる前にすぐ解放されます。これは、`allocations`カウンタが反復の開始時に一時的に2に増加し、終了時に1に減少することを意味します。問題は、バンプアロケータは**すべての**割り当てが解放された時、つまり`allocations`カウンタが0に減ったときにのみメモリを再利用できるということです。これはループの間には起こらないため、各ループ反復で新しいメモリ領域が割り当てられ、結果として大量の反復の後にメモリ不足エラーを引き起こします。 この失敗が発生する理由を詳しく理解してみましょう。まず、ヒープの先頭に変数`long_lived`の割り当てが作成され、`allocations`カウンタが1増加します。ループの反復ごとに、一時的な割り当てが作成され、次の反復が始まる前にすぐ解放されます。これは、`allocations`カウンタが反復の開始時に一時的に2に増加し、終了時に1に減少することを意味します。問題は、バンプアロケータは**すべての**割り当てが解放された時、つまり`allocations`カウンタが0に減ったときにのみメモリを再利用できるということです。これはループの間には起こらないため、各ループ反復で新しいメモリ領域が割り当てられ、結果として大量の反復の後にメモリ不足エラーを引き起こします。
#### テストを成功させるには #### テストを成功させるには
@@ -439,20 +439,20 @@ Error: panicked at 'allocation error: Layout { size_: 8, align_: 8 }', src/lib.r
#### 解放されたすべてのメモリを再利用するには? #### 解放されたすべてのメモリを再利用するには?
[前回の記事][heap-intro]で学んだように、割り当ては任意の期間生存する可能性があり、どのような順序でも解放されます。これは、次の例に示すように、個数に上限のない、非連続な未使用メモリ領域を管理する必要があることを意味します: [前回の記事][heap-intro]で学んだように、割り当ては任意の期間生存する可能性があり、どのような順序でも解放されます。これは、次の例に示すように、個数に上限のない、非連続な未使用メモリ領域を管理する必要があることを意味します:
[heap-intro]: @/edition-2/posts/10-heap-allocation/index.ja.md#dong-de-dainamituku-memori [heap-intro]: @/edition-2/posts/10-heap-allocation/index.ja.md#dong-de-dainamituku-memori
![](allocation-fragmentation.svg) ![](allocation-fragmentation.svg)
この図は、ヒープの経時変化を示しています。最初は、ヒープ全体が未使用で、`next`アドレスは`heap_start`に等しいです1行目。その後、最初の割り当てが行われます2行目。3行目では、2つ目のメモリブロックが割り当てられ、最初の割り当ては解放されています。4行目ではたくさんの割り当てが追加されています。それらの半分は非常に短命であり、すでに5行目では解放されていますが、この行では新しい割り当ても追加されています。 この図は、ヒープの経時変化を示しています。最初は、ヒープ全体が未使用で、`next`アドレスは`heap_start`に等しいです1行目。その後、最初の割り当てが行われます2行目。3行目では、2つ目のメモリブロックが割り当てられ、最初の割り当ては解放されています。4行目ではたくさんの割り当てが追加されています。それらの半分は非常に短命であり、すでに5行目では解放されていますが、この行では新しい割り当ても追加されています。
5行目が根本的な問題を示していますサイズの異なる未使用のメモリ領域が5つありますが、`next`ポインタはそのうち最後の領域の先頭を指すことしかできません。たとえば今回なら、長さ4の配列に、ほかの未使用メモリ領域の開始アドレスとサイズを保存することはできます。しかし、未使用メモリ領域の数が8個とか16個、1000個にもなる例だって簡単にできてしまうので、これは一般的な解決策ではありません。 5行目が根本的な問題を示していますサイズの異なる未使用のメモリ領域が5つありますが、`next`ポインタはそのうち最後の領域の先頭を指すことしかできません。たとえば今回なら、長さ4の配列に、ほかの未使用メモリ領域の開始アドレスとサイズを保存することはできます。しかし、未使用メモリ領域の数が8個とか16個、1000個にもなる例だって簡単に作れてしまうので、これは一般的な解決策ではありません。
普通、要素数に上限がないときは、ただヒープに割り当てられたコレクションを使います。これは私たちの場合には実際には不可能です──なぜなら、ヒープアロケータが自分自身に依存するのは不可能ですから(無限再帰やデッドロックを起こしてしまうでしょう)。なので別の解決策を見つける必要があります。 普通、要素数に上限がないときは、ヒープに割り当てられたコレクションを使ってしまえばいいです。これは私たちの場合には実際には不可能です──なぜなら、ヒープアロケータが自分自身に依存するのは不可能ですから(無限再帰やデッドロックを起こしてしまうでしょう)。なので別の解決策を見つける必要があります。
## <ruby>連結<rp> (</rp><rt>リンクト</rt><rp>) </rp></ruby>リストアロケータ ## <ruby>連結<rp> (</rp><rt>リンクト</rt><rp>) </rp></ruby>リストアロケータ
アロケータを実装する際、任意の数の空きメモリ領域を管理するためによく使われる方法は、これらの領域自体を管理領域として使用することです。この方法は、未使用メモリ領域もまた仮想アドレスにマッピングされており、対応する物理フレームも存在するが、そこに保存された情報はもはや必要ない、ということを利用します。解放された領域に関する情報をそれらの領域自体に保存することで、追加のメモリを必要とせずにいくらでも解放された領域を管理できます。 アロケータを実装する際、任意の数の空きメモリ領域を管理するためによく使われる方法は、これらの領域自体を管理領域として使用することです。この方法は、未使用メモリ領域もまた仮想アドレスにマッピングされており、対応する物理フレームも存在しはするが、そこに保存された情報はもはや必要ない、ということを利用します。解放された領域に関する情報をそれらの領域自体に保存することで、追加のメモリを必要とせずにいくらでも解放された領域を管理できます。
最もよく見られる実装方法は、解放されたメモリの中に、各ノードが解放されたメモリ領域であるような一つの連結リストを作るというものです: 最もよく見られる実装方法は、解放されたメモリの中に、各ノードが解放されたメモリ領域であるような一つの連結リストを作るというものです:
@@ -512,11 +512,11 @@ impl ListNode {
} }
``` ```
この型は`new`という単純なコンストラクタ関数を持ち、表現する領域の開始・終端アドレスを計算するメソッドを持っています。`new`関数は[const関数][const function]としていますが、これは後で静的な連結リストアロケータを作る際に必要になるためです。const関数においては、あらゆる可変参照の使用`next`フィールドを`None`にすることも含はunstableであることに注意してください。コンパイルを通すためには、`#![feature(const_mut_refs)]``lib.rs`の最初に追加する必要があります。 この型は`new`という単純なコンストラクタ関数を持ち、表現する領域の開始・終端アドレスを計算するメソッドを持っています。`new`関数は[const関数][const function]としていますが、これは後で静的な連結リストアロケータを作る際に必要になるためです。const関数においては、あらゆる可変参照の使用`next`フィールドを`None`にすることも含はunstableであることに注意してください。コンパイルを通すためには、`#![feature(const_mut_refs)]``lib.rs`の最初に追加する必要があります。
[const function]: https://doc.rust-lang.org/reference/items/functions.html#const-functions [const function]: https://doc.rust-lang.org/reference/items/functions.html#const-functions
`ListNode`構造体を部品として使えば`LinkedListAllocator`構造体を作ることができます: `ListNode`構造体を部品として使うことで`LinkedListAllocator`構造体を作ることができます:
```rust ```rust
// in src/allocator/linked_list.rs // in src/allocator/linked_list.rs
@@ -588,15 +588,15 @@ impl LinkedListAllocator {
} }
``` ```
このメソッドはメモリ領域のアドレスと大きさを引数としてり、リストの先頭にそれを追加します。まず、与えられた領域が`ListNode`を格納するのに必要なサイズとアラインメントを満たしていることを確認します。次に、ノードを作成し、それを以下のようなステップでリストに追加します: このメソッドはメモリ領域のアドレスと大きさを引数としてり、リストの先頭にそれを追加します。まず、与えられた領域が`ListNode`を格納するのに必要なサイズとアラインメントを満たしていることを確認します。次に、ノードを作成し、それを以下のようなステップでリストに追加します:
![](linked-list-allocator-push.svg) ![](linked-list-allocator-push.svg)
Step 0は`add_free_region`が呼ばれる前のヒープの状態を示しています。Step 1では、`add_free_region`メソッドが図において`freed`と書かれているメモリ領域で呼ばれました。最初のチェックを終えると、このメソッドは[`Option::take`]メソッドを使ってノードの`next`ポインタを現在の`head`ポインタに設定し、これによって`head`ポインタは`None`に戻ります。 Step 0は`add_free_region`が呼ばれる前のヒープの状態を示しています。Step 1では、`add_free_region`メソッドが図において`freed`と書かれているメモリ領域で呼ばれました。初期チェックを終えると、このメソッドは[`Option::take`]メソッドを使ってノードの`next`ポインタを現在の`head`ポインタに設定し、これによって`head`ポインタは`None`に戻ります。
[`Option::take`]: https://doc.rust-lang.org/core/option/enum.Option.html#method.take [`Option::take`]: https://doc.rust-lang.org/core/option/enum.Option.html#method.take
Step 2では、このメソッドは新しく作られた`node``write`メソッドを使って解放されたメモリ領域の先頭に書き込みます。次に`head`ポインタがこの新しいノードを指すようにします。解放された領域は常にリストの先頭に挿入されていくので、結果として生じるポインタ構造はいささか混沌としているように思われますが、`head`ポインタからポインタをたどっていけば、それぞれの解放領域に到達できるというのには変わりありません。 Step 2では、このメソッドは新しく作られた`node``write`メソッドを使って解放されたメモリ領域の先頭に書き込みます。次に`head`ポインタがこの新しいノードを指すようにします。解放された領域は常にリストの先頭に挿入されていくので、結果として生じるポインタ構造はいささか混沌としているように思われますが、`head`ポインタからポインタをたどっていけば、解放されたそれぞれの領域に到達できるというのには変わりありません。
[`write`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.write [`write`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.write
@@ -626,7 +626,7 @@ impl LinkedListAllocator {
current.next = next; current.next = next;
return ret; return ret;
} else { } else {
// 割り当てに適していない -> 次の領域で続ける // 割り当てに適していない -> 次の領域で繰り返す
current = current.next.as_mut().unwrap(); current = current.next.as_mut().unwrap();
} }
} }
@@ -677,7 +677,7 @@ impl LinkedListAllocator {
let excess_size = region.end_addr() - alloc_end; let excess_size = region.end_addr() - alloc_end;
if excess_size > 0 && excess_size < mem::size_of::<ListNode>() { if excess_size > 0 && excess_size < mem::size_of::<ListNode>() {
// 領域の残りが小さすぎてListNodeを格納できない割り当ては // 領域の残りが小さすぎてListNodeを格納できない割り当ては
// 領域を使う部分と解放されている部分に分けるので、この条件が必要) // 領域を使用部と解放部に分けるので、この条件が必要)
return Err(()); return Err(());
} }
@@ -689,7 +689,7 @@ impl LinkedListAllocator {
まず、この関数は行おうとしている割り当ての開始・終端アドレスを、先ほど定義した`align_up`関数と[`checked_add`]メソッドを使って計算します。オーバーフローが起こったり、(割り当ての)終端アドレスが領域の終端アドレスよりも後ろにあったりした場合は、割り当ては領域に入りきらないのでエラーを返します。 まず、この関数は行おうとしている割り当ての開始・終端アドレスを、先ほど定義した`align_up`関数と[`checked_add`]メソッドを使って計算します。オーバーフローが起こったり、(割り当ての)終端アドレスが領域の終端アドレスよりも後ろにあったりした場合は、割り当ては領域に入りきらないのでエラーを返します。
その後でこの関数の行うチェックは、先ほどのものほど自明ではありません。このチェックが必要になるのは、多くの場合適した領域にも割り当てがぴったりフィットするわけではないので、割り当て後も一部の領域が使用可能なままになるからです。領域のこの部分は割り当て後も自分自身の`ListNode`を格納しなければならないので、それが可能なくらいのサイズがないといけません。このチェックはまさにそれを確かめています:割り当てが完璧にフィットするか(`excess_size == 0`)、または`ListNode`を格納するのに十分超過領域が大きいかを調べています。 その後でこの関数は、必要な理由がやや分かりにくいチェックを行っています。このチェックが必要になるのは、多くの場合適した領域にも割り当てがぴったりフィットするわけではないので、割り当て後も一部の領域が使用可能なままになるからです。領域のこの部分は割り当て後も自分自身の`ListNode`を格納しなければならないので、それが可能なくらいのサイズがないといけません。このチェックはまさにそれを確かめています:割り当てが完璧にフィットするか(`excess_size == 0`)、または`ListNode`を格納するのに十分超過領域が大きいかを調べています。
#### `GlobalAlloc`を実装する #### `GlobalAlloc`を実装する
@@ -733,15 +733,15 @@ unsafe impl GlobalAlloc for Locked<LinkedListAllocator> {
} }
``` ```
`dealloc`メソッドのほうが単純なのでこちらから見ていきましょう:このメソッドではまず、何かしらのレイアウト調整(すぐ後説明します)を行っています。その次に、`&mut LinkedListAllocator`という参照を[`Locked`ラッパ][`Locked` wrapper]の[`Mutex::lock`]関数を呼ぶことによって取得します。最後に、`add_free_region`関数で割り当て解除された領域をフリーリストに追加します。 `dealloc`メソッドのほうが単純なのでこちらから見ていきましょう:このメソッドではまず、何かしらのレイアウト調整(すぐ後説明します)を行っています。その次に、`&mut LinkedListAllocator`という参照を[`Locked`ラッパ][`Locked` wrapper]の[`Mutex::lock`]関数を呼ぶことによって取得します。最後に、`add_free_region`関数で割り当て解除された領域をフリーリストに追加します。
`alloc`メソッドはもう少し複雑です。(`dealloc`と)同じようにレイアウト調整を行い、[`Mutex::lock`]でアロケータの可変参照を得るところから始めます。次に`find_region`メソッドを使って割り当てに適したメモリ領域を見つけ、それをリストから取り除きます。これが成功せず`None`が返された場合、適したメモリ領域がないため、(このメソッドは)`null_mut`を返すことでエラーを表します。 `alloc`メソッドはもう少し複雑です。(`dealloc`と)同じようにレイアウト調整を行い、[`Mutex::lock`]でアロケータの可変参照を得るところから始めます。次に`find_region`メソッドを使って割り当てに適したメモリ領域を見つけ、それをリストから取り除きます。これが成功せず`None`が返された場合、適したメモリ領域がないため、(このメソッドは)`null_mut`を返すことでエラーを表します。
成功した場合、`find_region`メソッドは適した領域(すでにリストにはない)と割り当ての開始アドレスからなるタプルを返します。(それを受け、`alloc`は)`alloc_start`と割り当てのサイズ、および領域の終端アドレスを使うことで、割り当ての終端アドレスと超過サイズを再び計算します。もし超過サイズがゼロでないなら、`add_free_region`を呼んでメモリ領域の超過サイズをフリーリストに戻します。最後に、`alloc_start`アドレスを`*mut u8`ポインタにキャストして返します。 成功した場合、`find_region`メソッドは(リストからすでに除かれた)適した領域と、割り当ての開始アドレスからなるタプルを返します。(それを受け、`alloc`は)`alloc_start`と割り当てのサイズ、および領域の終端アドレスを使うことで、割り当ての終端アドレスと超過サイズを再び計算します。もし超過サイズがゼロでないなら、`add_free_region`を呼んでメモリ領域の超過サイズをフリーリストに戻します。最後に、`alloc_start`アドレスを`*mut u8`ポインタにキャストして返します。
#### レイアウト調整 #### レイアウト調整
で、`alloc``dealloc`両方の最初に行っていたレイアウト調整はいったい何なのでしょうか?これらは、それぞれの割り当てブロックが`ListNode`を格納することができることを保証するものです。これが重要なのは、このメモリブロックはいつか割り当て解除されることになるので、そのときそこに`ListNode`を書き込む必要が出てくるからです。ブロックが`ListNode`より小さかったり正しいアラインメントがなされていなかったりすると、未定義動作につながります。 ……で、`alloc``dealloc`両方の最初に行っていたレイアウト調整はいったい何なのでしょうか? これらは、それぞれの割り当てブロックが`ListNode`を格納することができることを保証しているのです。これが重要なのは、このメモリブロックはいつか割り当て解除されることになるので、そのときそこに`ListNode`を書き込む必要が出てくるからです。ブロックが`ListNode`より小さかったり正しいアラインメントがなされていなかったりすると、未定義動作につながります。
レイアウト調整は`size_align`関数によって行われています。この定義は以下のようになっています: レイアウト調整は`size_align`関数によって行われています。この定義は以下のようになっています:
@@ -773,7 +773,7 @@ impl LinkedListAllocator {
### 使ってみる ### 使ってみる
これで`allocator`モジュール内の`ALLOCATOR`静的変数を新しい`LinkedListAllocator`更新できるようになりました 今や`allocator`モジュール内の`ALLOCATOR`静的変数を新しい`LinkedListAllocator`置き換えられます
```rust ```rust
// in src/allocator.rs // in src/allocator.rs
@@ -785,7 +785,7 @@ static ALLOCATOR: Locked<LinkedListAllocator> =
Locked::new(LinkedListAllocator::new()); Locked::new(LinkedListAllocator::new());
``` ```
`init`関数はバンプアロケータでも連結リストアロケータでも同じように振る舞うので、`init_heap`内における`init`関数の呼び出しを修正する必要はありません。 `init`関数はバンプアロケータでも連結リストアロケータでも同じ振る舞いをするようにしたので、`init_heap`内における`init`関数の呼び出しを修正する必要はありません。
`heap_allocation`テストをもう一度実行すると、バンプアロケータでは失敗していた`many_boxes_long_lived`テストを含めすべてのテストをパスします: `heap_allocation`テストをもう一度実行すると、バンプアロケータでは失敗していた`many_boxes_long_lived`テストを含めすべてのテストをパスします:
@@ -821,11 +821,11 @@ many_boxes_long_lived... [ok]
#### 性能 #### 性能
前述したように、バンプアロケータはとんでもなく速く、ほんの数個のアセンブリ命令に最適化することができます。この点でいうと、連結リストアロケータの性能はずっと悪いです。問題は、割り当ての要求に対し、適したブロックが見つかるまで連結リスト全体を調べ上げる必要があるかもしれないことです。 前述したように、バンプアロケータはとんでもなく速く、ほんの数個のアセンブリ命令に最適化することができます。これらと比べると、連結リストアロケータの性能はずっと悪いです。問題は、割り当ての要求に対し、適したブロックが見つかるまで連結リスト全体を調べ上げる必要があるかもしれないことです。
リスト長は未使用のメモリブロックの数によって決まるので、プログラムごとに性能は大きく変わります。いくつかしか割り当てを行わないプログラムは、割り当ての性能が比較的よいと感じることでしょう。しかし、大量の割り当てでヒープを断片化させてしまうプログラムの場合、連結リストがとても長くなり、そのほとんどがとても小さなブロックしか持たないということになるので、割り当ての性能は非常に悪くなってしまうでしょう。 リスト長は未使用のメモリブロックの数によって決まるので、プログラムごとに性能は大きく変わります。いくつかしか割り当てを行わないプログラムは、割り当ての性能が比較的よいと感じることでしょう。しかし、大量の割り当てでヒープを断片化させてしまうプログラムの場合、連結リストがとても長くなり、そのほとんどがとても小さなブロックしか持たないということになるので、割り当ての性能は非常に悪くなってしまうでしょう。
この性能の問題は、私たちの実装が簡素なせいで起きているのではなく、連結リストを使った方法の根本的な問題であるということに注してください。アロケータの性能はカーネルレベルのコードにとって非常に重要になるので、ここからは第三のアプローチ──性能を向上する代わりに、メモリの利用効率を犠牲にするもの──を見ていきましょう。 この性能の問題は、私たちの実装が簡素なせいで起きているのではなく、連結リストを使った方法の根本的な問題であるということに注してください。アロケータの性能はカーネルレベルのコードにとって非常に重要になるので、ここからは第三のアプローチ──性能を向上する代わりに、メモリの利用効率を犠牲にするもの──を見ていきましょう。
## 固定サイズブロックアロケータ ## 固定サイズブロックアロケータ
@@ -833,13 +833,13 @@ many_boxes_long_lived... [ok]
### 導入 ### 導入
**固定サイズブロックアロケータ**の背後にある発想は以下のようなものです要求された量ぴったりのメモリを返す代わりに、いくつかのブロックサイズを決めて、割り当てのサイズを次のブロックサイズに切り上げるようにするのです。たとえば、ブロックサイズを16,64,512バイトとしたら、4バイトの割り当ては16バイトのブロックを、48バイトの割り当ては64バイトのブロックを、128バイトの割り当ては512バイトのブロックを返します。 **固定サイズブロックアロケータ**の背後にある発想は以下のようなものです要求された量ぴったりのメモリを返す代わりに、いくつかのブロックサイズを決めて、割り当てのサイズを次のブロックサイズに切り上げるようにするのです。たとえば、ブロックサイズを16, 64, 512バイトとしたら、4バイトの割り当ては16バイトのブロックを、48バイトの割り当ては64バイトのブロックを、128バイトの割り当ては512バイトのブロックを返します。
連結リストアロケータと同じように、未使用メモリ部に連結リストを作ることによって未使用メモリを管理します。しかし、様々なブロックサイズのブロックを持つ一つのリストを使う代わりに、それぞれのサイズクラスごとに別のリストを作ります。それぞれのリストは一つのサイズのブロックのみを格納するのです。例えば、ブロックサイズが16, 64, 512のとき、3つの別々の連結リストがメモリ内にできます 連結リストアロケータと同じように、未使用メモリ部に連結リストを作ることによって未使用メモリを管理します。しかし、様々なブロックサイズのブロックを持つ一つのリストを使うのではなく、それぞれのサイズクラスごとに別のリストを作ります。それぞれのリストは一つのサイズのブロックのみを格納するのです。例えば、ブロックサイズが16, 64, 512のとき、3つの別々の連結リストがメモリ内にできます
![](fixed-size-block-example.svg). ![](fixed-size-block-example.svg).
`head`ポインタも一つではなく、`head_16``head_64``head_512`という、対応するサイズの最初の未使用ブロックを指す3つのポインタがあることになります。一つのリスト内のードはすべて同じサイズです。たとえば、`head_16`ポインタから始まるリストには16バイトのブロックのみが含まれます。これが意味するのは、ヘッドポインタの名前でそれぞれのリストのードサイズは指定されているので、ード内にそれらを格納する必要はないということです。 `head`ポインタも一つではなく、`head_16`, `head_64`, `head_512`という、対応するサイズの最初の未使用ブロックを指す3つのポインタがあることになります。一つのリスト内のードはすべて同じサイズです。たとえば、`head_16`ポインタから始まるリストには16バイトのブロックのみが含まれます。これが意味するのは、ヘッドポインタの名前でそれぞれのリストのードサイズは指定されているので、ード内にそれらを格納する必要はないということです。
リスト内のそれぞれの要素は同じサイズを持っているので、割り当ての要求に要素が適しているかはすべての要素について同じです。これは、以下の手順をとることで非常に効率的に割り当てを行えるということを意味します: リスト内のそれぞれの要素は同じサイズを持っているので、割り当ての要求に要素が適しているかはすべての要素について同じです。これは、以下の手順をとることで非常に効率的に割り当てを行えるということを意味します:
@@ -859,7 +859,7 @@ many_boxes_long_lived... [ok]
割り当てと同様、割り当ての解除もとても重要です。以下の手順をとります: 割り当てと同様、割り当ての解除もとても重要です。以下の手順をとります:
- 解放された割り当てサイズを次のブロックサイズに切り上げる。これが必要になるのは、コンパイラが`dealloc`に渡してくるのは要求された割り当てサイズであり、`alloc`によって返されたブロックのサイズではないためである。`alloc``dealloc`で同じサイズ修正関数を使うことで、正しい量のメモリを解放していることは保証される。 - 解放された割り当てサイズを次のブロックサイズに切り上げる。これが必要になるのは、コンパイラが`dealloc`に渡してくるのは要求したときの割り当てサイズであり、`alloc`によって返されたブロックのサイズではないためである。`alloc``dealloc`で同じサイズ修正関数を使うことで、正しい量のメモリを解放していることは保証される。
- リストのヘッドポインタを手に入れる。 - リストのヘッドポインタを手に入れる。
- ヘッドポインタを更新することで、解放されたブロックをリストの先頭に追加する。 - ヘッドポインタを更新することで、解放されたブロックをリストの先頭に追加する。
@@ -939,7 +939,7 @@ pub struct FixedSizeBlockAllocator {
[merge freed blocks]: #jie-fang-saretaburotukuwojie-he-suru [merge freed blocks]: #jie-fang-saretaburotukuwojie-he-suru
`FixedSizeBlockAllocator`を作るには、他のアロケータ型に実装したのと同じ`new`関数と`init`関数を実装すればいです: `FixedSizeBlockAllocator`を作るには、他のアロケータ型に実装したのと同じ`new`関数と`init`関数を実装すればいです:
```rust ```rust
// in src/allocator/fixed_size_block.rs // in src/allocator/fixed_size_block.rs
@@ -965,7 +965,7 @@ impl FixedSizeBlockAllocator {
} }
``` ```
`new`関数`list_heads`配列を空のノードで初期化し、`fallback_allocator`として[`empty`]で空の連結リストアロケータを作るだけです。`EMPTY`定数が必要なのは、Rustコンパイラに配列を定数値で初期化したいのだと伝えるためです。配列を直接`[None; BLOCK_SIZES.len()]`で初期化するとうまくいきません──なぜなら、そうするとコンパイラは`Option<&'static mut ListNode>``Copy`トレイトを実装していることを要求するようになるのですが、そうはなっていないからです。これは現在のRustコンパイラの制約であり、将来解決するかもしれません。 `new`関数がするのは、`list_heads`配列を空のノードで初期化し、`fallback_allocator`として[`empty`]で空の連結リストアロケータを作ることだけです。`EMPTY`定数が必要なのは、Rustコンパイラに配列を定数値で初期化したいのだと伝えるためです。配列を直接`[None; BLOCK_SIZES.len()]`で初期化するとうまくいきません──なぜなら、そうするとコンパイラは`Option<&'static mut ListNode>``Copy`トレイトを実装していることを要求するようになるのですが、そうはなっていないからです。これは現在のRustコンパイラの制約であり、将来解決するかもしれません。
[`empty`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.empty [`empty`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.empty
@@ -1049,7 +1049,7 @@ unsafe impl GlobalAlloc for Locked<FixedSizeBlockAllocator> {
} }
``` ```
他のアロケータの時と同じく、`GlobalAlloc`トレイトをアロケータ型に直接実装するのではなく、[`Locked`ラッパ][`Locked` wrapper]を使って同期された内部可変性を追加しています。`alloc``dealloc`の実装は比較的長いので、以下で一つ一つ示していきます。 他のアロケータの時と同じく、`GlobalAlloc`トレイトをアロケータ型に直接実装するのではなく、[`Locked`ラッパ][`Locked` wrapper]を使って同期された内部可変性を追加しています。`alloc``dealloc`の実装は結構長いので、以下で一つ一つ示していきます。
##### `alloc` ##### `alloc`
@@ -1085,7 +1085,7 @@ unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
一つ一つ見ていきましょう: 一つ一つ見ていきましょう:
まず、`Locked::lock`メソッドを使ってラップされたアロケータのインスタンスへの可変参照を手に入れます。次に、ついさっき定義した`list_index`関数を呼んで与えられたレイアウトに対して適切なブロックサイズを計算し、`list_heads`配列の対応するインデックスを得ます。これが`None`だったなら、割り当てに適したブロックサイズはないので、`fallback_alloc`関数を使って`fallback_allocator`を使います。 まず、`Locked::lock`メソッドを使ってラップされたアロケータのインスタンスへの可変参照を手に入れます。次に、ついさっき定義した`list_index`関数を呼んで与えられたレイアウトに対して適切なブロックサイズを計算し、`list_heads`配列の対応するインデックスを得ます。これが`None`だったなら、割り当てに適したブロックサイズはないので、`fallback_alloc`関数を使って`fallback_allocator`を使います。
もしリストのインデックスが`Some`なら、`list_heads[index]`から始まる対応するリストから[`Option::take`]メソッドを使って最初のノードを取り出すことを試みます。リストが空でないなら、`match`文の`Some(node)`節に入り、(ふたたび[`take`][`Option::take`]を使って)`node`の次の要素を取り出しリストの先頭のポインタとします。最後に、取り出された`node`ポインタを`*mut u8`として返します。 もしリストのインデックスが`Some`なら、`list_heads[index]`から始まる対応するリストから[`Option::take`]メソッドを使って最初のノードを取り出すことを試みます。リストが空でないなら、`match`文の`Some(node)`節に入り、(ふたたび[`take`][`Option::take`]を使って)`node`の次の要素を取り出しリストの先頭のポインタとします。最後に、取り出された`node`ポインタを`*mut u8`として返します。
@@ -1126,7 +1126,7 @@ unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
} }
``` ```
`alloc`と同じように、まず`lock`メソッドを使ってアロケータの可変参照を得て、`list_index`関数で与えられた`Layout`に対応するブロックリストを得ます。インデックスが`None`なら、`BLOCK_SIZES`にはサイズの合うブロックサイズがなかった、つまりこの割り当てが代替アロケータによって行われたことを意味します。従って、の[`deallocate`][`Heap::deallocate`]をつかってメモリを解放します。このメソッドは`*mut u8`ではなく[`NonNull`]を受け取るので、先にポインタを変換しておく必要があります(ここの`unwrap`はポインタがヌル値だったときのみ失敗するのですが、コンパイラが`dealloc`を呼ぶときにはそれは決して起きないはずです)。 `alloc`と同じように、まず`lock`メソッドを使ってアロケータの可変参照を得て、`list_index`関数で与えられた`Layout`に対応するブロックリストを得ます。インデックスが`None`なら、`BLOCK_SIZES`にはサイズの合うブロックサイズがなかった、つまりこの割り当てが代替アロケータによって行われたことを意味します。従って、代替アロケータの[`deallocate`][`Heap::deallocate`]を使ってメモリを解放します。このメソッドは`*mut u8`ではなく[`NonNull`]を受け取るので、先にポインタを変換しておく必要があります(ここの`unwrap`はポインタがヌル値だったときのみ失敗するのですが、コンパイラが`dealloc`を呼ぶときにはそれは決して起きないはずです)。
[`Heap::deallocate`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.deallocate [`Heap::deallocate`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.deallocate
@@ -1137,12 +1137,12 @@ unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
いくつか注目すべきことがあります: いくつか注目すべきことがあります:
- 私たちは、ブロックリストによって割り当てられたブロックと代替アロケータによって割り当てられたブロックを区別していません。これにより、`alloc`で作られた新しいブロックは`dealloc`でブロックリストに追加されるので、そのサイズのブロックの数は増えることになります。 - 私たちは、ブロックリストによって割り当てられたブロックと代替アロケータによって割り当てられたブロックを区別していません。これにより、`alloc`で作られた新しいブロックは`dealloc`でブロックリストに追加されるので、そのサイズのブロックの数は増えることになります。
- 私たちの実装において、`alloc`メソッドが新しいブロックが作られる唯一の場所です。つまり、最初は空のブロックリストから始めて、それらのブロックサイズの割り当てが行われたときに初めてリストを埋めていくということです。 - 私たちの実装において、新しいブロックが作られる唯一の場所`alloc`メソッドです。つまり、最初は空のブロックリストから始めて、それらのブロックサイズの割り当てが行われたときに初めてリストを埋めていくということです。
- `alloc``dealloc``unsafe`な操作を行っていますが、`unsafe`ブロックは必要ありません。これは、Rustは現在unsafeな関数の中身全体を大きな`unsafe`ブロックとして扱っているからです。明示的に`unsafe`ブロックを使うと、どの操作がunsafeなのかそうでないのかが明白になるという利点があるので、この挙動を変更する[RFCが提案](https://github.com/rust-lang/rfcs/pull/2585)されています。 - `alloc``dealloc``unsafe`な操作を行っていますが、`unsafe`ブロックは必要ありません。これは、Rustは現在unsafeな関数の中身全体を大きな`unsafe`ブロックとして扱っているからです。明示的に`unsafe`ブロックを使うと、どの操作がunsafeなのかそうでないのかが明白になるという利点があるので、この挙動を変更する[RFCが提案](https://github.com/rust-lang/rfcs/pull/2585)されています。
### 使う ### 使う
私たちのできたての`FixedSizeBlockAllocator`を使うには、`allocator`モジュールの`ALLOCATOR`静的変数を更新する必要があります: 私たちが今作った`FixedSizeBlockAllocator`を使うには、`allocator`モジュールの`ALLOCATOR`静的変数を更新する必要があります:
```rust ```rust
// in src/allocator.rs // in src/allocator.rs
@@ -1157,7 +1157,7 @@ static ALLOCATOR: Locked<FixedSizeBlockAllocator> = Locked::new(
`init`関数は、私たちの実装してきたすべてのアロケータで同じように振る舞うので、`init_heap`内における`init`関数の呼び出しを修正する必要はありません。 `init`関数は、私たちの実装してきたすべてのアロケータで同じように振る舞うので、`init_heap`内における`init`関数の呼び出しを修正する必要はありません。
`heap_allocation`テストをもう一度実行すると、すべてのテストがパスするはずです: `heap_allocation`テストをもう一度実行すると、すべてのテストが変わらずパスしているはずです:
``` ```
> cargo test --test heap_allocation > cargo test --test heap_allocation
@@ -1177,17 +1177,17 @@ many_boxes_long_lived... [ok]
- ブロックが必要になってから代替アロケータで割り当てる代わりに、リストを事前に埋めておき最初の割り当ての性能を向上させる方が良いかもしれません。 - ブロックが必要になってから代替アロケータで割り当てる代わりに、リストを事前に埋めておき最初の割り当ての性能を向上させる方が良いかもしれません。
- 実装を簡単にするため、2の累乗のブロックサイズのみを許すことで、ブロックサイズをアラインメントとしても使えるようにしました。アラインメントを別のやり方で格納するもしくは計算することで、任意の他のブロックサイズを使うこともできるでしょう。こうすると、より多くのブロックサイズ例えば、よくある割り当てサイズのものを追加でき、無駄になるメモリを最小化できます。 - 実装を簡単にするため、2の累乗のブロックサイズのみを許すことで、ブロックサイズをアラインメントとしても使えるようにしました。アラインメントを別のやり方で格納するもしくは計算することで、任意の他のブロックサイズを使うこともできるでしょう。こうすると、より多くのブロックサイズ例えば、よくある割り当てサイズのものを追加でき、無駄になるメモリを最小化できます。
- 現在、新しいブロックを作ることはしますが、それらを解放することはおこなっていません。これは断片化につながり、最終的には巨大な割り当ての失敗につながるかもしれません。それぞれのブロックサイズの最大リスト長を制限する方が良いかもしれません。最大長に達すると、その後の割り当て解除はリストに加える代わりに代替アロケータを使って解放するようにします。 - 現在、新しいブロックを作ることはしますが、それらを解放することはっていません。これは断片化につながり、最終的には巨大な割り当ての失敗につながるかもしれません。それぞれのブロックサイズの最大リスト長を制限する方が良いかもしれません。最大長に達すると、その後の割り当て解除はリストに加える代わりに代替アロケータを使って解放するようにします。
- 4KiB以上の割り当てについて、連結リストアロケータで代替するかわりに特別なアロケータを使うことが考えられます。発想としては、4KiBのページの上で動作する仕組みである[ページング][paging]を利用し、連続した仮想メモリのブロックを非連続な物理フレームへと対応づけるのです。こうすると、巨大な割り当てに関する未使用メモリの断片化はもはや問題ではなくなります。 - 4KiB以上の割り当てについて、連結リストアロケータで代替するかわりに特別なアロケータを使うことが考えられます。発想としては、4KiBのページの上で動作する仕組みである[ページング][paging]を利用し、連続した仮想メモリのブロックを非連続な物理フレームへと対応づけるのです。こうすると、巨大な割り当てに関する未使用メモリの断片化はもはや問題ではなくなります。
- このような「ページアロケータ」があるなら、ブロックサイズを4KiBまで増やし、連結リストアロケータはなくしてしまっても良いかもしれません。このやり方の利点は、断片化が少なくなり、性能の予測性が高まる──つまり、最悪の場合の性能がよりくなる──ことです。 - この「ページアロケータ」があるなら、ブロックサイズを4KiBまで増やし、連結リストアロケータはなくしてしまっても良いかもしれません。このやり方の利点は、断片化が少なくなり、性能の予測性が高まる──つまり、最悪の場合の性能がよりくなる──ことです。
[paging]: @/edition-2/posts/08-paging-introduction/index.ja.md [paging]: @/edition-2/posts/08-paging-introduction/index.ja.md
上で述べた実装の改善点は、あくまで提案に過ぎないということを忘れないでください。オペレーティングシステムのアロケータは、概してカーネル特有の作業のために高度に最適化されていますが、これは詳細なプロファイリングをしてこそ可能になるものなのです。 上で述べた実装の改善点は、あくまで提案に過ぎないということを忘れないでください。オペレーティングシステムのアロケータは、概してカーネル特有の作業のために高度に最適化されていますが、これは詳細なプロファイリングをしてこそ可能になるものなのです。
### 変化版 ### 亜種
また、固定サイズブロックアロケータの設計には多くの変化版があります。有名な例として**スラブアロケータ**と**バディアロケータ**の二つがあり、これらはLinuxのような有名なカーネルにおいても使われています。以下では、これらの二つの設計を軽く紹介します。 また、固定サイズブロックアロケータの設計には多くの亜種があります。有名な例として**スラブアロケータ**と**バディアロケータ**の二つがあり、これらはLinuxのような有名なカーネルにおいても使われています。以下では、これらの二つの設計を軽く紹介します。
#### スラブアロケータ #### スラブアロケータ
@@ -1222,7 +1222,7 @@ many_boxes_long_lived... [ok]
[linked list allocator]: @/edition-2/posts/11-allocator-designs/index.ja.md#lian-jie-rinkuto-risutoaroketa [linked list allocator]: @/edition-2/posts/11-allocator-designs/index.ja.md#lian-jie-rinkuto-risutoaroketa
[free list]: https://en.wikipedia.org/wiki/Free_list [free list]: https://en.wikipedia.org/wiki/Free_list
連結リスト方式の性能の問題を解決するため、決められたブロックサイズの集合を事前に定義しておく[固定サイズブロックアロケータ][fixed-size block allocator]を作りました。ブロックサイズごとに別々の[フリーリスト][free list]が存在するので、割り当て・割り当て解除はリストの先頭で挿入・取り出しを行えば良いだけになり、非常に速いです。 連結リスト方式の性能の問題を解決するため、決められたブロックサイズの集合を事前に定義しておく[固定サイズブロックアロケータ][fixed-size block allocator]を作りました。ブロックサイズごとに別々の[フリーリスト][free list]が存在するので、割り当て・割り当て解除はリストの先頭で挿入・取り出しを行えば良いだけになり、非常に速いです。それぞれの割り当てはそれより大きなブロックサイズに丸められるので、[内部断片化][internal fragmentation]によっていくらかのメモリが無駄になります。
[fixed-size block allocator]: @/edition-2/posts/11-allocator-designs/index.ja.md#gu-ding-saizuburotukuaroketa [fixed-size block allocator]: @/edition-2/posts/11-allocator-designs/index.ja.md#gu-ding-saizuburotukuaroketa