まるまるこふこふ

数々の次元が崩壊し、全ての生命が塵と化すのを見てきた。私ほどの闇の心の持ち主でも、そこには何の喜びも無かった。

【翻訳】goroutine の仕組み

訳者による概要

Krishna Sundarram 氏の記事「How Goroutines Work」の翻訳です。

「goroutine とは軽量スレッドである」という説明に対して抱くであろう 「どのようにして並行処理を実現しているのか」「既存のスレッド処理と何が違うのか」「なぜ軽量なのか」という疑問を解消する文章です。

とても良い文章なのですが、現在リンク切れになっており、 とてももったいないことだと思ったので、日本語に翻訳しました。

原文: How Goroutines Work (2017/12/02 現在、リンク切れ)

golang の紹介

もしあなたが golang 初心者で、並行処理(Concurrency)と並列処理(Parallelism)の違いがわからなければ、Rob Pike 氏のトーク (リンク先は英語)を参照してください。約 30 分のトークですが、30分視聴するだけの価値はあります。

違いを要約すると、「人が並行処理と聞いたときに想像するのは並列処理のことであり、それらは関連はしているがまったく別物である。平行処理とは、プロセス同士が独立して実行されることであり、並列処理はたくさんの計算を同時に実行すること(場合によっては互いに影響しあいながら)です」 並行処理とは一度にたくさんのことを扱うことであり、並列処理とは一度にたくさんのことを実行することです。*1

golang を使って、我々は並行処理プログラムを書くことができます。golang は、goroutine 及びそれら goroutine 同士が通信する仕組みを提供します。本文章では、前者の goroutine に焦点を当てます。

goroutine とスレッドの違い

Java がスレッドを使うように、golang では goroutine を使います。両者の違いは何でしょうか? この違いを語る上で 3つの要素があります。「メモリ消費量」「生成と破棄に要する時間」「スイッチングに要する時間」です。

メモリ消費量

goroutine は、メモリを大量に必要としません。スタック領域では 2kB しか消費せず、必要に応じてヒープ領域を割り当てたり開放したりします。*2*3一方でスレッドはスレッド間のメモリが干渉し合わないように「スタックガードページ」と呼ばれる 1Mb(goroutineの500倍) の領域の確保から始めます。*4

それゆえに、サーバーへリクエストがくるたびに、goroutine を生成するのは何も問題がないですが、スレッドでそれをやると、深刻な OutOfMemoryError が発生します。これは Java だけの制限ではなく、並行処理で OSのスレッド機構を使う全ての言語において、この問題に直面します。

生成と破棄に要する時間

スレッドは生成と破棄のたびに OS に要求を投げて、それが完了して返ってくるのを待つため時間がかかります。この問題を回避するには、一度生成したスレッドをスレッドプールに維持しておく必要があります。一方で goroutine では、生成と破棄に関する操作を非常に低コストで行うことができます。よって、golang では、goroutine を手動で管理する方法はサポートされていません。

スイッチングに要する時間

スレッドがブロックされると、別のスレッドがスケジューリングされます。 プリエンプティブ方式*5でスレッドがスケジューリングされ、スレッドの実行がスイッチされる際にスケジューラーは全てのレジスタ、つまり 16種類の汎用レジスタ、PC(プログラムカウンタ)、SP(スタックポインタ)、16種類の XMM レジスタ、 FP co-processor の状態、16種類の AVX レジスタ、そしてモデル固有のレジスタ etc... を別の場所に保存したり、保存したそれらをレジスタに戻す処理が必要になります。これはスレッド間で迅速にスイッチングしたい場合に、無視できません。

goroutine は協調してスケジューリングされ、スイッチングが発生したときも、たった 3つのレジスタ(PC、SP、DX)しか保存したり、レジスタに戻したりしません。これは非常に低いコストです。

以前に説明したように、goroutine の数は一般的にかなり多くの数になりますが、スイッチングにかかる時間に違いはありません。これには2つの理由があります。それは、実行可能な goroutine のみ考慮され、ブロックされた goroutine は考慮されないことと、また最近のスケジューラーは O(1) なため、実行可能な goroutine の数が、スイッチングの時間に影響を与えないからです。*6

goroutine がどのように実行されるか

前述したように、goroutine は生成から破棄までランライムが管理します。 ランタイムには goroutine が多重化された少数のスレッドが割り当てられます。 いつでも、それぞれのスレッドは1つの goroutine を実行します。goroutine がブロックされると、それはスワップアウトされ、別の goroutine が代わりにスレッドによって実行されます。*7

goroutine は協調してスケジューリングされるので、ループしつづける 1つの goroutine が、同一スレッド内の他の goroutine の実行を妨げることがあります。 Go 1.2 では、関数を実行する際に、Go のスケジューラーが呼び出されることがあるため、この問題は多少緩和されました。よって、ループ内でインライン関数でない関数を呼び出しておけば、他の goroutine がスケジューリングされます。

goroutine のブロック

goroutine は低コストであり、goroutine が以下の場合でブロックされた際に、それらが多重化されているスレッドのブロックを引き起こしません。

  1. ネットワーク入力
  2. sleep 処理
  3. channel 操作
  4. sync パッケージによるプリミティブのブロック

たとえ数万個の goroutine が生成され、それらがブロックされたとしても、 ランタイムが代わりに別の goroutine をスケジューリングするため、システムのリソースは無駄にしません。

簡単にいうと、goroutine はスレッドの軽量な抽象化です。 Go プログラマはスレッドを扱わず、また同様に OS は goroutine の存在を認識していません。OSから見れば、Go のプログラムは、イベント駆動の Cプログラムのように振る舞います。(脚注 6 参照)

スレッドとプロセッサ

ランタイムが生成するスレッドの数を直接制御することはできませんが、一方でプログラムが利用するCPUプロセッサの数を決めることはできます。 これは runtime.GOMAXPROCS(n) を呼び出して、変数 GOMAXPROCS を設定することで可能です。コアの数を増やしても、設計によっては必ずしもプログラムのパフォーマンスが上がるとは限りません。プロファイリングツールを利用して、プログラムの理想的なコア数を見つけることができます。

終わりに

他の言語と同様に、複数の goroutine によって、共有リソースの同時アクセスを防止することは大切です。goroutine 間でのデータのやり取りは、channel を通して行うのが最善です。共有メモリを通してデータのやり取りをするのではなく、代わりに channel を使って、メモリを共有します。*8

最後に、C. A. R. Hoare 氏による「Communicating Sequential Processes」を確認することを強くおすすめします。彼は本当の天才でした。1978年に発行されたこの論文で、彼はプロセッサの単一コアの性能が最終的に横ばいになり、チップメーカーは代わりにコア数を増やすことを予見しました。彼のプロポーザルの偉業は、Go 言語のデザインに深い影響を与えました。

*1:Concurrency is not parallelism」by Rob Pike

*2:Effective Go: Goroutines

*3:goroutine のスタック上でのサイズは、Go 1.4 から、8kB -> 2kB に減りました。参考

*4:Five things that make Go fast」 by Dave Cheney

*5:訳注 OSがプロセッサの実行権限を管理し,タスクの実行を切り替える方式

*6:Dmitry Vyukov 氏が、 golang-nuts グループにて gorouine のスケジューリングについて述べています。参考

*7:Analysis of the Go runtime scheduler」 by Deshpande et al.

*8:Share Memory By Communicating

技術書典3 で Emscripten 本出します。

f:id:sairoutine:20171020185653p:plain:w300

10月22日(日)に秋葉原UDXで開催される技術書典3にて

『Emscriptoon』を頒布します。

techbookfest.org

内容は Emscripten という C/C++ コードを JavaScript に変換するコンパイラを初心者向けに紹介した本です。

頒布価格は 500円。 サークルスペースは「い08」です。お手洗いの近くよ。

blog.techbookfest.org

入場に関しては整理券を配布するようです。 スマホで確認できるサイトがあるのでぜひ活用してみてくださいー。

blog.techbookfest.org

戦利品を片手に休憩できるスペースもあるみたいです。 数量限定でコーヒーとミネラルウォーターがあるって。

よろしくお願いします!

emscripten で DOSBox をブラウザで動かしてみた。

はじめに

f:id:sairoutine:20170731020004p:plain

Emscriptenとは C/C++言語からLLVMを生成し、それをJavaScriptに変換するコンパイラのことです。
DOSBoxは、PC/AT互換機MS-DOS環境を再現するエミュレータです。

結果

上記ツイート内動画のように、DOS向け Bad Apple!! をブラウザで再生することができました。

再生

皆様のブラウザでも、以下のURLから再生することができます。

https://sairoutine.github.io/BadAppleJS/

起動すると「Choose sound device」と聞かれるので「3」を押して Enter してください。ロードに成功すると、「Press enter to start」と言われるので、Enter を押してください。再生が始まります。

※「Exception thrown, see JavaScript console」と表示されたらリロードしてみてください
※音ズレが発生しているのは勘弁

過程

em-dosbox という DOSBox の emscripten 移植が存在するので、それを利用しています。

https://github.com/dreamlayers/em-dosbox

DOS 向け Bad Apple!! は以下のサイトからダウンロードできます。

http://abaduaber.ru/Prog.htm

(ロシア語でわかりづらいですが、「Bad Apple, ДОС - версия」という項目がソレです。)

事前にダウンロードして解凍し、のちのち git clone してくる ./em-dosbox/src 配下に移動させておきます。

コンパイル手順は基本的に em-dosbox レポジトリの Readme に書いてある通りです。環境は、MacOS X EI Captain 10.11.6 です。

なお、emsdk を利用しています。

# emsdk のセット
git clone git@github.com:juj/emsdk.git
cd emsdk-portable
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh 

# em-dosbox のダウンロード
cd ../
git clone git@github.com:dreamlayers/em-dosbox.git

cd em-dosbox

# configure ファイルの生成
./autogen.sh

# emscripten 用 Makefile の生成
emconfigure ./configure --without-sdl2

# コンパイル
make

# DOS 向け Bad Apple!! の実行ファイルを emscripten 向けにパッケージング
./packager.py badapple badapple BADAPPLE.EXE

# http-server を起動
python -m SimpleHTTPServer 8000

# ブラウザでアクセス
open http://localhost:8000/badapple.html

以上です。

ちょっと解説

emconfigure ./configure --without-sdl2

em-dosbox はデフォルトで、SDL2 を利用してコンパイルするが、 私のMac には SDL1 しか入ってなかったので、オプション指定して SDL1 を利用しています。

SDL2 が入っていないと、以下のようなエラーになる。

f:id:sairoutine:20170731020014p:plain

./packager.py badapple badapple BADAPPLE.EXE

packager の第一引数にはパッケージ後のファイル名を、第二引数にはパッケージ対象のディレクトリを、第三引数には、DOSBox をブラウザで開いた際に、最初に起動する実行ファイル名を指定します。

なお、実行ファイルが1つしか存在しない場合、以下のように、第二引数に直接実行ファイル名の指定もできます。

./packager.py badapple BADAPPLE.EXE

終わり

描画が追いついておらず、音ズレが発生しているのは、WebAssembly を有効にすれば 解決するかもしれない。次回やってみる。

Bad Apple !!だけでなく、DOSBox では例えば Win3.1 や Windows 98 も起動できるみたいなので、うまくやれば、ブラウザ上でそれらも動かせるかもしれない。

RPG アツマールに RPG ツクール MV 製ではないゲームをアップロードしてみた

弊サークル作品の「紫と霊夢の終わらない夏」の体験版を RPG アツマールに投稿できたので、アップロードにあたって、やったことをまとめます。

ゲームの概要

「紫と霊夢の終わらない夏」は PC向け 2D アクションパズルゲームです。RPGでもなければ、RPG ツクールMV 製でもありません。

技術的には、HTML5 + javaScript で開発されており、 Electron を使って、PC 向けにビルドして頒布しております。

アップロードのやり方

RPG アツマールは RPG ツクール製のゲームがアップロードできるサイトです。PRG ツクール製ゲームのアップロードに向いていますが、Web の技術で作られたゲームであれば、RPGツクール製でないゲームでもアップロード可能です。

RPG アツマールの仕様やアップロードの仕方については、 先人の方がいて、すでにまとめられてますので、以下のURLをご参照ください。

http://qiita.com/hajimehoshi/items/2a28b16a2e587c82ac5d

やったこと

Web ブラウザ上で動くゲームならそのままアップロードできるはずですが、 アツマールの仕様上、「紫と霊夢の終わらない夏」では以下の対応を行いました。

音楽ファイルを wav から m4a と ogg に変換

RPG ツクールMVが、m4a や ogg といった音楽ファイルしか使用できない制限のため、RPG アツマールでは、wav などの一部の音楽ファイルのアップロードが制限されています。

よって、wav ファイルを m4a と ogg の両方に変換し、ゲーム側で、 ogg あるいは m4a を再生しわけるように修正しました。

(Chrome では ogg が再生できますが、safari では ogg が再生できないので、m4a を使用します。)

index.htmlに埋め込みでjavascriptが書けない

セキュリティ上の理由で、index.html に javascript を記述できないので、javascript コードは全て js ファイルに記述して、script タグで読み込むように修正しました。

safari でフォントの読み込みが終わらない

これはRPGアツマールの仕様というより、Safari の挙動が怪しい話です。 弊ゲームでは以下のようなコードで、ゲームで使用するフォントの読み込みが完了するまで、ゲームをローディング画面で待機させています。

しかし、Safari のみ、loadingdone イベントが発火しません(バージョンは10.0)

 // フォントの読み込みが完了
    if(document.fonts) {
        document.fonts.addEventListener('loadingdone', function() { game.fontLoadingDone(); });
    }
    else {
        // フォントロードに対応してなければ無視
        game.fontLoadingDone();
    }

これは、以下のように safari だけブラウザ判定して、 フォント読み込み待ちをしないことで修正しました。

 // フォントの読み込みが完了
    if(document.fonts && !navigator.userAgent.toLowerCase().indexOf("safari")) {
        document.fonts.addEventListener('loadingdone', function() { game.fontLoadingDone(); });
    }
    else {
        // フォントロードに対応してなければ無視
        game.fontLoadingDone();
    }

.gitkeep が含まれていてアップロードできない

RPG アツマールにアップロードできるファイルは、 非常に限られており、未対応のファイルをアップロードすると、 アップロードした時点で、どのファイルが未対応か教えてくれます(便利)

私の場合、.gitkeep ファイルが残ったままだったのでアップロードに失敗しました。 これは、.gitkeep を全て削除して解決しました。

おしまい

RPG アツマールでは、特に規約上でもRPG ツクールMV 製でないゲームのアップロードは禁止されていません。

「紫と霊夢の終わらない夏」のようなオリジナルゲームエンジン製のゲームをアップロードすることもできますし、例えばティラノスクリプト製のノベルゲームをアップロードすることもできます。

みんなも Let’s try!

追記

RPGアツマールのスクリーンショット機能や、画面最大化機能が使えない。canvas DOM に振ったIDが悪い気がするので調査する

初めての個人ゲーム制作の振り返り

2016年12月29日のC91冬コミにて、東方Project の二次創作シューティングゲーム宇佐見蓮子の大事な約束」を頒布しました。 (DLsite でまだ DL購入できるので、興味ある方はぜひ購入してみてください。)

初めての個人ゲーム制作ということもあり、まずは「エターナらない」ことを目標に制作しました。

「エターナル」(英語の形容詞eternal:永遠の、果てしない)を動詞化させた造語。 ツクラー(ここでは主にPC用のRPGツクールのユーザー)の間で生まれた。 未完の大作「エターナルファンタジー」からとも。参考 作者が諸般の事情によりゲーム製作を途中で放棄すること、またはその状態を表す。

エターナるとは (エターナルとは) [単語記事] - ニコニコ大百科 より引用

ゲーム制作がエターナる原因は、主に以下のものがあります。

  • ゲームの構想が大きい
  • モチベーションが維持できない
  • 制作に当てる時間がない

宇佐見蓮子の大事な約束」では、これらを避けるために、 以下の方針で制作しました。

  • 大きなチャレンジをしない
  • モチベーションを折らない仕組み作り
  • 制作に必要なタスクを外部に委譲

大きなチャレンジをしない

宇佐見蓮子の大事な約束」は全5面のいわゆるオーソドックスな縦シューティングゲームであり、 プレイ時間 1時間もかからない程度のボリュームです。

いわゆる超大作になりがちなゲームジャンルや、あるいはプレイ時間の長さを目指すことによる 「作りきれない」を避けています。

また技術的には自分の得意な HTML5 を利用して制作しました。

宇佐見蓮子の大事な約束」はジャンルとしては、弾幕シューティングゲームであり、 PC向けシューティングゲームであれば、C++DirectX を使ったり、あるいは東方弾幕風といった シューティングゲーム制作スクリプトを使うのが一般的です。

ですが、1から学習をしないといけないため、学習にかかる時間が見積もりしづらいことと、 学習に対するモチベーションが続くか不明だったため、自分の得意分野である HTML5 を利用しました。

とはいえ、HTML5 においても GamePad API (ゲームコントローラの入力の取得) や FullScreen API (画面最大化)といった技術的なチャレンジはしていますが、 これらはおおよそ学習や調査の工数が見積もりやすいものです。

なお HTML5弾幕シューティングゲームが作れることは toho-like-js が既に存在するのでわかっていました。

モチベーションを折らない仕組み作り

複数人での開発(コアメンバーが自分含めて 2名、加えて外部の方に手伝って頂いております) だったため、 自他のモチベーションを維持するため、以下のことをやっていました。

適切な情報共有

Google Drive を利用し、制作した素材や、あるいは制作において決めたこと、共有したことを全てそこに集めるようにしました。 また現状の進捗がわかるように、ゲームをすぐに遊べる状態にしておきました。 (これは HTML5 で制作していてビルドの概念がないので、ある URL にアクセスすればいつでもすぐ遊べるようにできました。)

意思決定に参加してもらう

文字でのやり取りももちろん、なるべく口頭での相談、進捗擦り合わせを心がけました。

制作に必要なタスクを外部に委譲

主にドット絵やBGMなどを外部の方に制作をお願いさせていただきました。

6ヶ月という短いスパンで制作ができたのは、 1人で全て制作するのではなく、複数人で制作できたからだと思います。

また自分以外の人がゲームに関わることで、完成に対する責任感もできました。

完成版を頒布して良かったこと

当たり前のことですが、自分の作品を色んな方々に遊んでもらうことができました。 Twitterで感想を書いてくださる方もあり、自分だけでは気づかなかったゲームの課題なども知ることができました。

またニコニコ生放送などで実況プレイをしてくださる方もいました。 こちらは、実際にプレイしてくださる方が、ゲーム上のどの場面でどういう反応をするのか知ることができて、 本当に勉強になりました。

課題

「ゲームにおける面白いとは何か?」「その面白いを実現するためには?」といったことへの 考えが足りなかったと思います。

シューティングゲームのなにが面白いのかという理解が足りず、 レベルデザインのミスや、あるいは快適なプレイ感がなかったという反省があります。

また制作の方針としても、そもそものゲームのコンセプト(こういう点が今回のゲームの魅力的なところだ)ということが ボヤケていて、制作が進んでいく中で発生する意思決定にブレがあり、 制作に手戻りを発生させて、人に迷惑をかけたこともありました。

これらの反省を踏まえて、次回作では、このゲームはなにが面白いのかということをきちんと考えた上で コンセプトを決めて、そこをブレさせないことや、あるいは 自分以外の人にもっとたくさんテストプレイしてもらって、 ゲーム上での不快感を潰すことを行おうと思います。

最後に

f:id:sairoutine:20170625153122p:plain

(「宇佐見蓮子の大事な約束」のいわゆるトゥルーエンドで使用した画像)

ゲーム制作は楽しかったです。複数人で 0 からモノを作っていくことは楽しいです。 そうした楽しさは、自分もそうだし、あるいは今後自分のゲーム制作に関わってくれる人にも感じて頂けるよう努力します。

一方で、ゲームをプレイしてくれたプレイヤーを楽しませるための視点や知識、経験はもっと養っていきたいです。 今後も向上させていけるよう努めます。

作ることを制作者が楽しめて、そして作ったものでプレイヤーが楽しんでくれることが、 個人ゲーム制作の醍醐味だと思います。

台湾 例大祭の一般参加した時のメモ

第二回博麗神社例大祭 in 台湾に一般参加してきました。 日本国外である台湾での開催ということで、なかなかハードルが高いと思われがちですが、 案外アッサリ行ってこれたので、どんな準備をしたかを書こうと思います。

f:id:sairoutine:20170529184215j:plain

画像は台湾例大祭で購入した公式グッズ(おみやげ?) であるところのパイナップルケーキです。

パスポートを取得する。

これは自分は既にパスポートを取得してたので、今回は特になにもしてませんでした。 ググれば、パスポートの取得の仕方はノウハウが見つかるので、そちらを参照頂ければと思います。

Visa は不要

日本のパスポートがあれば、台湾への渡航に Visa は不要です。

飛行機のチケットの取得

LCC(格安航空券)を使うと安いと思います。 私は行こうと決めた時期が遅く、LCCがほぼ売り切れだったので、 China Airlines でチケットを購入しました。往復 6万円くらい。 LCCであれば、3万円くらいで行けると思います。

ホテルの予約

楽天トラベル(http://travel.rakuten.co.jp/)を利用して、現地のホテルを予約。 例大祭の会場が、台北市大同区だったので、"大同区"で検索して、 なるべく会場に近いホテルを予約した。 予約したホテルはビジネスホテルで、1泊 6000円くらいでした。

持ち物

パスポート、クレジットカード、現金、PC、スマホ、充電器、着替えだけ持っていった。 最悪、パスポート・クレカ・スマホさえあればなんとかなるので、 それらだけは絶対に忘れずに。

充電器は日本のものがそのまま使えるので、変圧器などは不要。

空港

関空から台湾 桃園国際空港まで飛行機でだいたい片道3時間くらい。 搭乗手続きに時間がかかるので、離陸時刻の1時間前には、 空港のカウンターでチェックインするくらいの時間感覚で、空港に来ること。 空港での手続きまでは、日本語が通じる。大丈夫。

両替

空港にて、日本円を台湾ドルに両替。空港が一番交換レートが良いと聞いた。 交換手数料が 30台湾ドル(約120円くらい)かかる。

僕は 7万円両替して 18000台湾ドル手に入れたが、結局 現地では 7000台湾ドル(約26000円)くらいしか使わなかった。

空港からホテルへの移動

桃園国際空港から台北市まで距離があるので、電車あるいはタクシーで移動することになります。 私はタクシーを使ったので、タクシーの話をします。 だいたい空港から台北市まで、タクシーでだいたい40分くらい。料金は約1000台湾ドル(約4000円)。

タクシーのおっちゃんは、日本語も英語も通じないことが多いので、 スマホとかに、行き先の住所をメモして見せると良い。

ホテルからイベント会場への移動

これもタクシー。市内であれば、100〜200台湾ドル(500円~1000円くらい)で移動できる。 大きな通りであれば、タクシーはめちゃ流れてるので、簡単に捕まえられる。 日本のタクシーと違って、台湾のタクシーは自動でドアが開かないので、自分で開けること。

イベント会場

今年の会場は以下でした。

会場名:卡市達創新基地(圓山店)
住所:台北市大同區承德路三段232號B2

以降の開催も同じ場所である保証はないので、今後の台湾例大祭がどこで開催されるかは、 公式サイトを必ずご確認ください。

基本的にはイベント会場での注意事項は日本と同様です。

列形成のイベントスタッフは台湾語の人ばかりで、そこだけ面食らうかもしれません。 10時半開催のところ、私は10時に来たのですが、だいぶ列が出来てました。

列で並んでるとスタッフが参加者の手にスタンプを押し始めます。 カタログを事前購入していない人は、ここで100台湾ドル払って、スタッフにスタンプを手に押してもらいます。 入場は、カタログあるいは手のスタンプを会場スタッフに見せることで入場できます。

カタログは200台湾ドルですが、会場でカタログを購入する場合に、 手のスタンプを見せると、100台湾ドルでカタログが購入できます。

会場は人も多いし、熱気が凄かった。

FAQ

台湾って日本語は通じるの?

あまり通じないって聞いてます。ホテルは高いホテルだと通じるかも。 イベント会場では、台湾のサークル主の方は、簡単な日本語であれば通じる人もいました。

台湾って英語は通じるの?

タクシーはほぼ通じない。 ホテルは簡単な英語なら通じるといった感じでした。

Wi-Fi状況は?

ホテル/空港はほぼほぼFree Wi-Fi。 さすがに道端とかで フリー Wi-Fi は飛んでいないので、 お店とかに入る必要がある。

会場での同人誌のお値段

漫画本だと、100台湾ドル(約400円)〜150台湾ドル(約600円)くらい。 イラスト本だと 300台湾ドル (約1200円)とかが多かった。

Electron で手動GCを行う

さいです。
JavaScript と Electron でPC向けの弾幕シューティングゲームを作っています。
C++ と違って JavaScript はGarbage Collection (以降GCと呼称)がある言語なので、
ゲームプレイ中に不定期にGCが発生して、ゲームのパフォーマンスが悪化することがあります。

Object pooling を使って、GCの回数は減らす努力はしているのですが、
それでも(恐らく)GCの発生によって、古いPC等では露骨に画面が止まることが
あるので、Electron上で GCのタイミングを調整したいなぁと思いました。

Electron は Chromium ベースのプラットフォームであり、
Chromium は v8 エンジンを使用しています。
v8 エンジンのGCは Mark-Sweep 方式でのGCです。

v8 エンジンが自分で判断して行うGCを止める方法は
調べたのですが、無さそうです。

よって仮説として、メモリが潤沢にある環境であれば、オブジェクトが必要なくなってもすぐにオブジェクトへの参照を切らずにしておき、リザルト画面などのタイミングで不要なオブジェクトへの参照を切って、それからすぐに手動でGCを発生させることで、ゲームプレイに支障が出る場面でのGCをなくせそうです。

というわけでやっとこの記事の本題に入りますが、Electron 上で
手動GCを行う方法を書きたいと思います。

–expose-gc フラグを立てる

const electron = require('electron');
const app = electron.app;
app.commandLine.appendSwitch('js-flags', '--expose-gc');

app.on('ready', () => {
  // Your code here
})

app.commandLine.appendSwitch('js-flags', '--expose-gc'); を実行することで、Electron のレンダラプロセス上で global.gc() 関数を実行することができるようになります。

メインプロセス上のコードで app に対して、ready イベントが発火するまえに、app.commandLine.appendSwitch() を実行することで、
js-flags に引数を渡すことで、Electron の v8 エンジンに対して--expose-gc フラグを渡すことができます。

この辺は、公式の docs に記載されている。

github.com

global.gc() の実行

上記の方法で、v8 エンジンに --expose-gc フラグを渡すことで、
Electron のレンダラプロセス上で、global.gc() が実行できるようになります。

// 手動GCの実行
if (global.gc) {
    global.gc();
}

サンプルコード

サンプルコードを書いて、GCが行われているか確認してみます。

メインプロセス

// electron エントリポイント
'use strict';
const electron = require('electron');
const app = electron.app;
const dialog = electron.dialog;
const BrowserWindow = electron.BrowserWindow;
const globalShortcut = electron.globalShortcut;

app.commandLine.appendSwitch('js-flags', '--expose-gc');


let mainWindow;

function createWindow () {
        mainWindow = new BrowserWindow({
            "width":          640,
            "height":         480,
        });

    mainWindow.loadURL(`file://${__dirname}/index.html`);
    // Open the DevTools.
    mainWindow.webContents.openDevTools();

    mainWindow.on('closed', function () {
        mainWindow = null;
    });
}

app.on('ready', createWindow);

app.on('will-quit', () => {
    // Unregister all shortcuts.
    globalShortcut.unregisterAll();
});

app.on('window-all-closed', function () {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

app.on('activate', function () {
    if (mainWindow === null) {
        createWindow();
    }
});

レンダラプロセス

'use strict';

var Game = function() {
    this.frame_count = 0;

    this.bullets = [];
};
Game.prototype.run = function() {
    requestAnimationFrame(this.run.bind(this));

    this.frame_count++;

    // create
    for (var i = 0; i < 100000; i++) {
        this.bullets.push(new Bullet());
    }
    // run
    for (var j = 0; j < this.bullets.length; j++) {
        this.bullets[j].run();
    }

    // reset
    this.bullets = [];

    if (this.frame_count % 600 === 0) {
        if (global.gc) {
            console.log("done gc");
            global.gc();
        }

        console.log(process.memoryUsage().heapUsed);
    }
};

var id = 0;
var Bullet = function() {
    this.id = id++;
    this.frame_count = 0;
};
Bullet.prototype.run = function (){
    this.frame_count++;
};

window.onload = function() {
    var game = new Game();
    game.run();
};

レンダラプロセスは、1フレーム毎に、Bullet オブジェクトを10万個生成しては、その場で破棄しています。
また10秒に1度、GC を実行して、それからヒープ領域の使用量を console.log で出力しています。

f:id:sairoutine:20170513220817p:plain

上記が実行結果です。10秒ごとに GC が実行されているため、
そしてGC 実行後のヒープ領域は常に最小になっています。

f:id:sairoutine:20170513220823p:plain

こちらが、js-flags を追加しなかった場合です。
追加しなかった場合、レンダラプロセスには global.gc() 関数は存在しないので、
GC は実行されずに、ヒープ領域の使用量も不定(レンダラプロセス側の v8 エンジン任せ) となっています。

最後に

この記事を書いていて思ったのですが、メモリ解放対象のオブジェクトがなくとも、
GC時のルートオブジェクトからマーク済みオブジェクトを探索していく処理が、
ゲームに影響を与えるレベルの処理だと上述の方法は意味ないですね…

v8 エンジンが何を持ってGCを走らせてるのか知りたいなと思いました。