借り初めのひみつきち

仮ブログです。

今週の MYOS

さいきんの MYOS/TOE 界隈のトピックです。

WebAssembly 高速化 (fused op)

中間コード変換とスタック操作合理化によって以前に比べると結構速くなりました。 この2つの改良によって実現可能になった新しい高速化手法があります。

WebAssembly は所謂スタックマシンなので個別の命令はスタックに push/pop するだけの単純な動作を行う命令が多く、実際のコードでは定型の組み合わせがたくさん出現します。

例えばローカル変数 i1 を加算する処理は以下のようなバイトコードの組み合わせになり、 WebAssembly では似たようなバイトコードの並びがたくさん出現します。

  local.get $i    ;; ローカル変数 $i の値をスタックに push
  i32.const 1     ;: 定数 1 をスタックに push
  i32.add         ;; スタックから2つの値を pop して加算した結果をスタックに push
  local.set $i    ;; スタックの値を pop してローカル変数 $i にセット

特に i32.const の直後に i32.add が続くようなスタックに push した値をすぐに pop して加工する組み合わせはひとまとめに処理した方が無駄なメモリアクセスが減って動作効率が向上します。

インタプリタ実装では仕様書で定義されているバイトコードを1つずつ読み込んで実行することしかできませんでしたが、現在のインタプリタは一旦中間命令に変換して実行しているのでインタプリタの動作に必要な新しい中間命令を追加することができます。 そこで、いくつかの定型句に関しては統合してひとつの中間命令 (fused op) に変換し、まとめて実行することで効率化しました。

これによってベンチ上の数値もだいぶ上昇しました。 同様に頻度の高い定型句を統合していけば全体的な動作速度が向上していくと思います。

スレッドのメモリリーク

メモリ管理にはいくつかのレイヤーがありますが、ここでは主に所有権とライフタイムの話になります。

Rust では GC を使わずコンパイラが所有権とライフタイムの管理をするため参照に厳しい制限があることで有名です。 しかし、コンパイラがいくら頑張っても対応できないシチュエーションに対応するため、 RcArc というリファレンスカウントによるスマートポインタもサポートしています。

リファレンスカウント方式のメモリ管理では参照するたびに参照カウンタを1つ増やして参照を保持している全員が所有権を共有し、参照を破棄する際に参照カウンタを1つ減らして0になったら全ての参照が破棄されてオブジェクトのライフタイムが終了して解放されます。 動作が単純で循環参照以外のほとんどのユースケースをカバーできるため多くの場面で同様の手法が使われています。

MYOS ではスレッドのデータを他のスレッドから参照中にスレッドが終了してデータが迷子にならないように Arc を使用しています。

今まで終了処理をあまり真面目に考えていませんでしたが、以前からスレッドの終了時に解放処理がうまく動いていない雰囲気は察していました。 気になって調べてみると、スレッド終了しても誰かがスレッドデータを参照してカウントが0にならないため解放されていませんでした。

自分で自分を消す処理というのは実現不可能なので、 MYOS のスケジューラーは最終的にスレッドステータスの終了フラグを立ててコンテキストスイッチを実行して、コンテキストスイッチ後の後片付け処理の中で終了済みスレッドのデータを解放してもらいます。 この時点で自分で自分の参照を保持していてもスレッドが永久に再開されないので参照も永久に解放されずスレッドのメモリリークが発生します。

コンテキストスイッチをするためにはスレッドのコンテキストデータにアクセスするためにスレッドデータの参照が必要です。 通常の場合はコンテキストスイッチが終わると参照が解放されますが、スレッドが終了するときは先述のように自分自身のスレッドデータへの参照を保持したまま最期のコンテキストスイッチするので解放されません。

また、スレッドからランタイムのインスタンスを取得する Scheduler::current_personality() でも Personality の参照を保持している間はスレッドを間接的に参照して保持しているので、この中でスレッドを終了するとスレッドが解放されません。

そもそも、現在実行中のスレッド自身のスレッドデータがもしも解放されてしまったら現在のスレッドを参照する様々な機能が動作しなくなるし、次回のコンテキストスイッチも実行できずにシステムがクラッシュしてしまうので、スケジューラー内のスレッドプールがスレッドを保持していて絶対に解放されることがありません。 つまり、現在実行中のスレッドが自分自身への参照で参照カウントする必要性がありません。

ということで参照カウントしないようにスレッドの参照を取得する機能を追加しました。 これによってスレッド終了時に Drop が呼び出されるようになって単純なアプリではスレッドのメモリリークは解消されたと思います。

しかし、複雑なアプリではまだメモリリークがありました。 アプリをロードだけして実際には実行しないような実験をしてもリークすることがあったので、色々な場面に細かいメモリリークが潜んでいるようでさらに調査が必要です。

また、メモリリークとはちょっと違いますが、現在のページアロケーターはメモリを解放してもフリーリストに戻す処理が未実装なので、メモリリークを起こさないアプリでも何度も起動しているとそのうちフリーエリアが枯渇する問題があります。この辺もそのうち改良していきたいです。

メモリリーク先生の戦いはこれからだ!