Second Life in Japan
Advertisement

この文書は Jam Meili氏による英文資料を同氏の許可のもと翻訳・掲載したものです。オリジナルの英文との差異を生じないようにするため、文書中に疑義・修正がある場合は直接の編集を避け、注記マークをつけた上でノートまたはMono Howtoへ誘導するようにお願いします --- Nock Forager 2008年9月16日 (火) 16:03 (UTC)

Monoへの最適化手法[]

この技術の基礎となっている背景を説明するにあたり、出来る限りシンプルに理解しやすいように書こうと注意しました。上級もしくは中級のスクリプターの方にとって、また(Monoを)より理解しよう・知識を深めようとしているかたにとって有益な情報となることを望みます。

ひとことで言うと、この文書に書いてあるような手法はシムのラグを軽減するヒントになります。特に多くの人が同じようなものを使用しているような場面では効果が大きくなります。例えば武器などの作者がある程度限られているようなものや、ロールプレイ用のHUDなどがそれにあたります。武器については多くの弾丸をrezするような場面ではさらに効果が大きくなります。

この文書をよりよくするための提案や、文書の書き方そのもの(英語は私のメインの言語ではないので!)について何かあれば Jam Meiliまで気軽にIMしてください。

Monoそれ自体は必ずしも速いわけではありません。が、内部要素を扱う方法がより効率的になっています。そうは言っても非効率なコードは書けてしまいます。Monoによる性能向上を最大に引き出すためにはMonoがどのように動作しているのか、その背景を知ることが大切です。

C++、C#(Monoの基礎となっている言語です)、アセンブリ言語など10以上の言語を10年以上にわたってリアル世界でのプログラマーとしてやってきた経験、コードのデバックやメモリ管理・コンパイラの動作、OS上でコードやメモリがどのように扱われるかの知識を元に SLのMonoエンジンについてどうすれば最適化ができるか記載したいと思います。

MonoベースのLSLコンパイラは今回提供されなくなりましたが、リアルのアプリケーションで使用されているC#/Monoでのコードやデータの取り扱いから考えると、LSLコンパイラもそれらと大差ないものと類推することができます。Monoの開発責任者のBabbage LindenがMonoの構造や背景についていくつか情報提供や確認をしてくれたことはこの文書を書くにあたって大変参考になりました。

ソフトウェアの話をする際、最も重要な部分はメモリ管理です。なぜかというと、ホトンドのバグ、クラッシュ、セキュリティホールはメモリ管理に関係しているからです。良いプログラムはマシンのメモリを大量に使ったりせず、必要最小限で稼動するものです。ハードディスクへのスワップが発生したり、メモリを空けるための処理がかかったりすると動作の何もかもが遅くなります。つまりSLでいうところの「ラグ」にもメモリは関係しているわけです。Monoでは従来と比較して4倍のメモリ空間を使用できることから、このメモリ管理に関する問題は以前よりもより重要になったと言えるでしょう。


さて、LSLにおいて私たちはメモリを空けたり管理したりする必要はありません。端的に言えば私たちにはそれらのプロセスを管理する方法がホトンドありません。LSLでは変数を定義できるのみで、それを開放するすべはありません。C++などでは気を使わなくてはいけないこれらのことも、LSLではエンジン側がすべてやってくれます。このことはよりエラーが発生しにくい手法といえますが、同時にメモリに対するコントロールができないことで、より多くのメモリを消費してしまう傾向にも繋がっています。そういった制限の中でも、Monoエンジンがどのようにメモリを取り扱っており消費しているかを理解すれば、その使用状況に大きな影響を与えることができます。(従来のLSLエンジン - 以降ではLSOと表記します - にはそういった管理機構は不足していました)。次項ではLSOとMonoエンジンとでどのような差が生じているのか、どうすればより良い状態で稼動するスクリプトが作れるのかについて説明します。


LSOとMonoの最大の違い[]

まず、LSOとMonoのもっとも大きな違いはスクリプトから使用できるメモリ量です。LSOでは16Kバイトであった制限がMonoでは64Kバイトまで拡張されています。
LSOでは個々のスクリプトに対してそれぞれ16Kバイトのメモリが割り当てられていました。これはスクリプトのコードやデータ量にかかわらず一律でした。ひとつひとつではたいしたことがなくても、それらが1000個、1万個あればシム全体としては大きな差異になります。Monoではスクリプトが必要とするメモリ量が動的に割り当てられますので、LSOでの場合に比較してMonoでコンパイルされたスクリプトはおそらくより少ないメモリで稼動することになるでしょう。また、LSOではメモリ割当は静的な処理でしたが、Monoでは稼働状況に応じて動的に割当が行われるようになっています。
以上の事柄から以下のようなTipsが導かれます:


1.スクリプトは分割するのではなく、ひとつのスクリプトにより多くのコードを実装するべきです。大きなサイズのソースコードであっても従来よりもメモリ消費は少なくなるはずです。ただし、機能的な都合によって小さなスクリプトを分けなくてはいけない場合であってもメリットは効いてきます。
  • より少ないメモリで動作します。いくつかの内部機構が(それぞれのスクリプト用に存在せず)ひとつだけで済むからです。
  • ロードおよび初期化のコード、メモリやスレッド管理が少なくてすみます。ライブラリコールやアセット・データベース操作が減り事でCPUタイムやおそらくネットワーク・タイムの消費も減少します。このあたりが特に効いてくる場合(弾丸など)はrezするのも速くなる効果があるでしょう。
  • オールインワンの構成にして複数のスクリプト間で通信を行わないことで、100倍の速度向上も場合によっては望めるでしょう。
  • スクリプト間でデータを共有する処理が必要なくなります。もしいくつかのスクリプトがそれぞれに値を共有する処理をしていたならば、これがなくなる分で全体としての消費量が減るはずです。


2.必要のない変数を減らしたり複数の目的で変数を再利用しましょう。多くの変数を定義するとそれだけメモリも割り当てられてしまいます。LSOではあまり関係がなかったことですが、Monoではこれらのことにも意味がでてきます。


LSOから向上したポイント[]

LSOとMonoで大きく向上したことのひとつに、メモリへスクリプトがロードされるときのしくみがあります。LSOではコンパイルされたバイトコードはインスタンスごとにそれぞれに対して個別にロードされるしくみでした。Monoでコンパイルされたスクリプトはいわゆる .NETアセンブリ(原理的にはライブラリや DLLのようなものです)となりOSのより現代的なローディング処理を使用できるようになります。現代的なソフトウェアアプリケーションではコードとデータセグメントは分けられています。ライブラリがロードされた場合、それは他の多数のアプリケーションからもロードされる場合があるということです。コード自体はメモリ内で変化することがないからできることです。複数のアプリケーションがそのコードを要求したとしてもメモリ内にコードはひとつだけで済むのです。データセグメントだけがそれぞれのアプリケーションごとに割り当てられます。
このことはいわゆるコンピュータアプリケーションがメモリ上のコードを共有する仕組みに似ています。インワールドのオブジェクトもコードベースを共有することができるわけです。簡単に言うとアセンブリ(スクリプトのバイナリコードですね)は1度だけロードされればよく、それを使用している複数のオブジェクトで共有して使用されることになります。唯一、インスタンスごとに分けて用意される必要があるのは変数だけです。この原理をうまく利用することができれば同じシム内に同一スクリプトコードからコピーされたスクリプトが数千合った場合などには、MonoとLSOでメモリ消費量に大きな違いがでてきます。


以上の事柄から以下のようなTipsが導かれます。どうすることが寄り効率的か:
3.アセンブリにはソースコードは含まれておらずコンパイルされた結果のみ、実行用のマシンコードしかないことに注意してください。システムはあなたの書いたソースコード単位ではなくアセンブリ単位で稼動することになるため、場合によっては まったく同一のソースコードを元にしているのに必要以上に重複してメモリにロードされてしまうということが起こりえます。これを避けるために知っておいて欲しいこと、それはコンパイル処理をするたびに内部的には別の新しいアセンブリが作成されるということです。もしスクリプトをコピーして、同じものをそれぞれをコンパイルしたら、エンジンにとっては技術的にそれらは別のものとして扱われるわけです。同じコードを複数のオブジェクトで使用したい場合、それぞれに新しいスクリプトを作ってソースコードをコピーしてコンパイルするということはしてはいけません。そうではなくスクリプトをコンパイルして、それをインベントリにコピーし、それが必要なオブジェクトにはインベントリにコピーしたものをコピーしていれるようにする必要があります。コンパイルしなおしてはいけません。


4.スクリプトが1度しかロードされないようにできれば、シムサーバーは余計なメモリを使用しなくてすみます。ひいてはリソースの余裕も増加しますし、ハードディスクへのスワップをしなくて良くなる分のラグの減少にもつながります。スクリプトが、たったひとつのオブジェクトに対してでも一度でもロードされているならば、それはハードディスクから読み直したり初期化したりする必要はないのです。これは弾丸をrezするようなrezzerなどの場合に特に効果がでてくるでしょう。


5.もしどうしても必要な場合は、シム内に存在するコードはrezされているオブジェクトのインベントリ直下に存在するようにしてください(オブジェクトの中にあるオブジェクト内のスクリプト、ということがないように)。最初の一回目のrezは初期化処理の分時間を食ってしまいますが、このようにスクリプトを配置することで、同じスクリプトが繰り返しrezされる物のような場合、初期化処理を少し省略することが出来る分、初期化自体がすこし早くなる効果があります。


6.同一シム内で広く使われるようなコード(例えばコンバットシムでの銃弾のような)を共通化することでスクリプトの稼動状況を向上することが出来ます。スクリプトを作る際に、例えば他の武器と同じ機能やサブパートになっている部分を統合したりして より汎用化・統合化をすることもできるでしょう。同一コードを使うけれどもオブジェクトの名前やインベントリ内のオブジェクトを判定して異なる武器として必要な動作だけをするようにするということもできるでしょう。製品で言えばライトバージョンとプロバージョンなどのタイプ違いなどにもこういったことが適用できます。
注意:ソースコードが同一だったとしても、再コンパイルをしてしまうと、それは別のアセンブリになってしまい、前のものとは共有化されなくなるということに注意してください。ソースコードに修正をしなくてはいけない場合以外は最初にコンパイルしたバージョンをコピーしてコードを共有化するようにしてください。
共有化を勧めることはとても重要なポイントです。単にメモリに影響するだけではなく様々な点でよい作用がでてきます。例えばCPUキャッシュのレベルでも、LSOがまったくキャッシュが効かなかったのに比較して向上することが期待できます。可能な限り共有化をすることでスクリプトが消費するcpu time全体も減る可能性もあるということです。


7.全てのスクリプトを再コンパイルしなくてはいけない場合を除いて、"Tools -> Recompile scripts in Selection"は使用しないようにしましょう。全部をコンパイルし直してしまうことで、せっかく共有化できていたスクリプトが製品のバージョン間で切り離してしまうことになります。自分が流通させているスクリプトのバージョンに気を使って、コードが変更にならない限り以前から流通させているものと同一の再コンパイルさせていないものを使うように注意しましょう。スクリプトの名称や詳細説明の欄を使ってリビジョン番号などを管理して、どれがどれだかわかるようにしていきましょう。


ひとつ明確にしておくポイントがあります:異なるシム間ではコードの共有化は行われません。これはシムがそれぞれ別のシミュレータ上で稼動しているからです。この場合、相互にはまったく影響は生じません。


Monoのメモリ管理について[]

Monoではメモリが動的に割り当てられるようになりました。次にこの点から検討を行ってみましょう。リアルでのアプリケーションでは速度向上のための手法として、小さなアプリケーションの場合はライブラリを多用してその都度呼び出しを行うということは避けて、かわりに事前にメモリへ読み込んでしまうという手があります。LSLではメモリの割当や開放を実際にはコントロールすることはできないですが、これはどのように適用できるでしょうか。


8.一時的な変数をたくさん設定することを避けましょう。例えばイベントや関数ごとに変数を定義して使うといったようなことです。さらにその関数が頻繁に呼び出されるようなものだとさらに悪い影響がでてしまいます。コードを見直してあまりおかしくならない範囲で、繰り返しコールされるような状態を避けたり、グローバル変数を使用したり、変数の再利用をしたりすると良いでしょう。可能ならループに入る前に変数を初期化するようにしましょう。これでどれくらい差がでるのか?と疑問に思うことでしょう。添付のサンプル(3つのベンチマークテストを入れてあります)を使って自分自身で確認してみてください。再コンパイルしてみる以外に、単純にスクリプトをリセットするのも試してみると、初回と二回目でキャッシュが効いて動作が100倍ほども高速になることがわかるでしょう。また、最後の手法が必ず一番早く動作することも確認できると思います。(テストはほとんどなにもないシムで行いました。高負荷下の環境では、必ずしも同じ結果にならない可能性があります)。
Mono Benchmark variable usage オブジェクトに入れて、再コンパイルして、タッチして実行です。
LSOでも動作させてみてください。1番目のテストはMonoの3倍程度、2番目のテストは10倍は長くかかります。また、LSOではMonoのようにキャッシュが効かないため二回目の実行でもホトンド速度向上は見られません。Monoでキャッシュが効いている場合と比較すると240倍というようなものすごい速度差があらわれてきます
(TIP:サンプルを流用して自分独自のベンチマークを作ってみましょう!なおテストをする際は必ず複数回、それもある程度十分な回数テストを実施するようにしてください。その際には単にスクリプトをリセットするのではなくコンパイルしなおすようにしましょう。そうすることで適切で評価に値する結果が得られるようになります。)


9.単純だが効果的な方法:オブジェクトに含まれている必要のないスクリプトをすべて消してください。例えばフローティングテキストを表示したり(llSetText)、パーティクルをセットしたり(llParticleSystem)、テクスチャをセットしたり、色やプリムの各種パラメータを定義したり、オブジェクトを回転させたり(llTargetOmega)、テクスチャをアニメーションさせたり(llSetTextureAnim)。こういったもので、1度しか使用しないようなものも働き続けているということを意識して、必要がないスクリプトは削除するように意識するべきです。何もしていない idle状態のスクリプトでも cpu-timeとメモリを消費します。MonoではLSOよりもこの影響が大きくなっています。もし将来の変更のためなどの理由でスクリプトを残しておきたいのであれば、"Running"ではなく停止させておくことも検討してください。実際、動作させておく必要はないはずです。


誤った解釈に注意:[]

使用できるメモリが増えたからといって、従来の四倍のリストを使えるとは考えてはいけません。これは必ずしも当てはまりません。これを理解するためにはMonoがLSOとは異なって、どのようにデータタイプを取り扱っているかについて知る必要があります。
LSOでは内部的に文字列はUTF-8のコードとして格納されていました。これはつまり目に見えるキャラクタは 1から3バイトのメモリを占めるということです。MonoではそれらはUTF-16で格納されます。つまりキャラクタごとに2バイト消費するということです。単純なアルファベット、数字、特殊キャラクタの付かない句読点、などはLSOと比較してMonoでは倍のメモリを消費することになります。したがって最悪の場合、LSOと比較して使用できるメモリ量は2倍までということになってしまいます。
内部的な格納方法はリストの場合でも異なっており、MonoではLSOと比較してエントリごとに少し多くのメモリを消費します。したがって単純に使用できるメモリが4倍になった!と考えてしまうと、実際に使用できるリストの長さは あなたが思っているよりも少ないという結果になる可能性があります。リンデンラボが今回メモリ上限をLSOスクリプトの4倍に増やした理由もここにあります。上限を上げることによって大きなリストを必要としいる既存のコンテンツが破綻しないようにしたのです。また、floatやkey、stringではなくintegerを使うようにしましょう。テストでもstringはkeyよりも効率的という結果が出ています。
この文章を記述している時点でグローバル変数が倍のメモリを消費してしまうバグが存在していますので注意してください。この影響で、非常に大きなリストを使用する場合、LSOで大丈夫だったものがMonoでは稼動しないという場合があります。これは近いうちに修正される予定です。(http://jira.secondlife.com/browse/SVC-2715)


また、最適化のテクニックは必ずしも常に有効とは限らないことにも注意してください。多くの場合、これらの効果は対象のコードに依存します。すべての場合についてカバーできるようなテクニックを見出すにはあまりにも条件が多岐に渡りすぎています。コードを駆使してLSLで可能な範囲でプロファイリングを行いベンチマークをすることで初めてどのようなコードがベストなのか知ることができます。その一方で、たった一度しか実行されなかったり、メモリの差や効果がホトンドないようなものに対して多大な労力をかけて調査するのはナンセンスともいえるでしょう。
コードの最適化を行うアルゴリズムは実世界のソフトウェアに利用されている方法を参考にするのが役に立ちます。例を挙げると、同じ答えを返す関数を繰り返し繰り返し呼び出すのではなく、答えを変数に格納してループの中ではこれを繰り返し使用する、などのコーディングにおいて一般的に言われているようなテクニックはLSLでも同じように効果があるわけです。
もう一点、LSLコードの最適化手法として挙げられている手法、特にlslwiki.netに示されているようなものはLSOエンジンのバイトコード処理に依存しています。実のところLSOのコンパイラは最適化はほとんどしていませんでした。これはMonoにはあまりあてはまりません。有名な例では例えばリストメモリーに関するハックは意味がなくなっています。これはLSOであったようなリスト管理でのメモリのムダがMonoでは発生しないからです。実際のところリストの実装方法は従来とはまったく異なっています。ということで、最適化の指南を参照する場合は、それがMono向けのものかどうか注意するようにしてください。


著者情報[]

Autor: Jam Meili、Date: 2008-09-04、Provided free for Members of the Group "SCRIPTS"

この文書を販売したりフリービーのパッケージに含めたりすることは禁止します。その代わりに上記のグループを参照するように促してください。

(C)opyright 2008 by Jam Meili - All Rights reserved.


Translation into Japanese done by Nock Forager. 翻訳に関する疑問点は Nock Foragerまでお願いします。

Advertisement