開発を進めていたある日のこと……。
に゛ょ゛っ……!?(動揺) これはメモリのアクセス違反のエラーで、要するにメモリが足りないということのようです。WASM-4でも死の青い画面なんですね。そういえばタイルマップを作ろうとして、やや大きめの配列を確保したのでした。WASM-4はメモリが64キロバイトしかないので、雑にメモリを使うと意外とあっさりメモリ不足に陥るようです。
せっかくなのでメモリ周りを観察してみましょう。F8キーを押すと開発ツールのウインドウが開き、メモリマップを確認できます。update
関数が空っぽのアプリだと現在のメモリ空間は黒い四角になっていますが、これはまだメモリ内の値がゼロばかりで、全然何も書き込まれていないということです。
スタックやヒープにメモリを確保してみます。Rustでは関数内で普通に配列を用意するとスタック上に確保されます。いっぽう、Vec
はヒープ上に確保されます。
const BUFFER_SIZE: usize = 128;
#[no_mangle]
fn update() {
let buf: [u8; BUFFER_SIZE] = [255; BUFFER_SIZE]; // スタック上に128バイトを確保して255で埋める
let vec1: Vec<u8> = vec![255; 128]; // ヒープ上に128バイト確保して255で埋める
}
上の方にある青い帯がスタック上に確保された配列ですね。下のほうにある青い帯がヒープ上のVec
ですね。中央部のガチャガチャしている部分は、文字列リテラルなどの静的なデータや、なにかの関数を呼び出しでメモリが使われたあとにスタックに残されたゴミかなにかでしょうか。配列のサイズを大きくしていくと、この中央部から上端に向かってスタックが伸びていきます。
せっかくなのでもっと限界まで確保してみます。詳細はよくわからないのですが、ヒープの方は一度に32768バイトまでしか確保できず、しかも確保するたびに次に確保できるサイズが減っていく感じだったので、何回かにわけて確保して限界に迫ってみます。
const BUFFER_SIZE: usize = 13350;
#[no_mangle]
fn update() {
let buf: [u8; BUFFER_SIZE] = [255; BUFFER_SIZE];
let vec1: Vec<u8> = vec![255; 32768];
let vec2: Vec<u8> = vec![255; 4096];
let vec3: Vec<u8> = vec![255; 2048];
let vec4: Vec<u8> = vec![255; 128];
let vec5: Vec<u8> = vec![255; 64];
let vec6: Vec<u8> = vec![255; 64];
let vec7: Vec<u8> = vec![255; 64];
}
これでメモリ空間の大半が255
で埋まりましたね。ここまでスタックを使うと、グラフィックメモリも完全に破壊されて、画面の上の方まで変なノイズがでてしまっています。こうなるともうソフトウェアがまともに動くことは期待できないでしょう。
メモリアロケーション
Rustのプロジェクトテンプレートからプロジェクトを作成すると、エントリポイントであるlib.rs
のほかに、alloc.rs
というファイルも作成されます。これはbuddy_alloc
というクレートを使ってメモリアロケータを初期化しているようです。詳細はよくわかりませんが、コメントにあるとおり、4キロバイトと16キロバイトのヒープを確保しているようです。ちなみにさっきの実験のときも、ここをいじってヒープを40キロバイトくらいまで増やしていました。
use buddy_alloc::{BuddyAllocParam, FastAllocParam, NonThreadsafeAlloc};
// These values can be tuned
const FAST_HEAP_SIZE: usize = 4 * 1024; // 4 KB
const HEAP_SIZE: usize = 16 * 1024; // 16 KB
const LEAF_SIZE: usize = 16;
static mut FAST_HEAP: [u8; FAST_HEAP_SIZE] = [0u8; FAST_HEAP_SIZE];
static mut HEAP: [u8; HEAP_SIZE] = [0u8; HEAP_SIZE];
#[global_allocator]
static ALLOC: NonThreadsafeAlloc = unsafe {
let fast_param = FastAllocParam::new(FAST_HEAP.as_ptr(), FAST_HEAP_SIZE);
let buddy_param = BuddyAllocParam::new(HEAP.as_ptr(), HEAP_SIZE, LEAF_SIZE);
NonThreadsafeAlloc::new(fast_param, buddy_param)
};
まあなんにしろ、教科書とかで読んだことのある、「スタックは逆向きに伸びていく」というのを実際に確認できて面白かったです。(ゲーム開発は……?)
次回予告
次回はもっと実践的なデバッグのやりかたを見ていきたいと思います。printfデバッグはつらいので、デバッガーはプログラミングを学ぶときに最初に用意するべきだと思うんですよ。