Boehm GC の調査中にわかったことを 開発メモのBoehm GCの項 に書き記していますが、なかなかいやらしいですね。
とくにファイナライザ(デストラクタ)が呼ばれるタイミングや、そもそも呼ばれるか呼ばれないかすら信用できなくなるというのは痛いです。
デストラクタで重要な処理を行ってるのはなんとか回避するしかないですね。
自分の書いたプログラムはいったんぜんぶ見直す必要がありそうです。STL のアロケータも GC のにしないとなぁ。ざっと見回した範囲ではデストラクタではメモリの解放しているところがほとんどだから、それを気にしなくてもよくなるというのは気楽でいいです。
参照カウンタによる GC は AddRef と Release が釣り合わないと悲惨になりましたが、これはいくつかの「ルール」を守ればトラブルになることはまずありませんでした。Boehm GC の「ルール」はどうもそれよりはトラブルを起こしやすそうな気がします。
とりあえずしばらく使ってみます。というかもうすでに真 Risse の文字列管理部分とか書き始めていますが ... ええ、GC のおかげですごい「書きやすい」です。書きやすいけど「うっかり」GCの知らない領域にポインタを放置してしまったら ... とか考えると不安になります。
Risse の開発に着手します ...
いま Risa の上で動いているのは TJS2 を半ば無理矢理移植した中途半端なスクリプト言語なのですが、このまま Risa を構築して気づいたときには (現状のTJS2ベースのRisseに依存しすぎて) 手遅れだったという事態を防ぐため、新 Risse の実装に着手しようと思います。
Trac の方に wiki ページを作成しておきました → Risse の仕様や設計・実装方針などを考えるページ
Trac の wiki ページの編集は誰でもできるようにしておきましたので、気づいた点などあったらページの最後の方のコメント欄にでも書いておいてくださいませ。
前に吉里吉里3の内部名称をRisaにすると言いましたが、なんか開発中ずっとコアエンジンをRisaと呼んでいるうちに頭の中での名称が完全にRisaに切り替わってしまったような。
Risaをコアエンジンの名称ということにして、吉里吉里3をコアエンジンや周辺ツールも含めたソフトウェアパッケージの名称ということにしたほうがスマートかな。未定。
やっと(スクリプト制御で)音が鳴った!ということで見てもビジュアル的につまらないスクリーンショット

いまのところ再生できるサウンド形式としては PCM 系しか考えていないので、クラス名も単純にいまのところ Sound とすることにしています。
それにしてもなんか音質が悪い。再生サンプリングレートが低いような。OpenALの初期化に問題があるような気がするので見直します。
OpenAL 1.1 Core SDK 付属の OpenAL のソフトウェアミキサが waveOutOpen でデバイスを開くときに22.05KHzで開いていました。CVS trunkの最新のやつでは 44.1KHz で開くようになったので CVS 版をコンパイルして入れておきました(チェンジセット1466)。
Solaris の次世代ファイルシステムである ZFS がアツいようなので簡単に紹介。すでにOpenSolarisやSolaris Expressで使用可能のよう。
とまあ、発展途上ながらなんか機能がてんこ盛りみたいです。
ファイルシステムはメタデータやデータの区別無く、すべてが一つのツリー構造になっている様子。整合性の保持には「何か書き込みを行う場合は必ず元データを新しい場所にコピーしてから」(copy-on-write)で、1つ版のあがったファイルシステムのイメージを作成し、古い版を指している一番頭のポインタを新しい版を指すように変更する(ここがアトミック)という方法を行っているようです (ここのスライド(PDF)がわかりやすいです)。これはディスク上のフォーマットの説明(PDF)。
この処理は何をやるにも毎回クローンを作ってから、最後にその新しいクローンを一つ前の古いクローンと置き換えているようなもので、スナップショットやクローンの機構はこれをそのまま使ってるようです。
どっかのファイルの1バイトを書き換える場合は、ファイルの元データブロックをコピーし、関係するツリーノードも親までさかのぼってコピーして更新して書き込むというI/Oが発生するようです。この意味ではRAID-Zでcopy-modify-writeが不要というのはちょっとまやかしっぽい?と思ったけど、RAID-5のように固定ブロックサイズを常にI/Oするのではなくて変更があれば変更のあった分だけのブロックサイズで書き込むのでより効率的ということなのでしょうか。
バルクな書き込みはともかく、既存ファイルの一部分だけを書き換えるようなある種の操作は従来型のファイルシステムに比べてかなり遅いんじゃないの?と思ったら、(僕が)心配していたような性能劣化はここを見る限り起きないようです(SolarisのUFSと比べて、ですが)。おおむね UFS より速いっぽいし、特定のデータアクセスパターンでは4~6倍速いらしいです。逆に特定のデータアクセスパターンではUFSに比べて数倍遅いですが、改善する余地があるとのこと。
まだ詳しくコードを追って調べた訳ではないので嘘書いてたらごめんなさい。
前述のシングルトン管理ですが、モジュール化にも非常に役に立っています。
singleton_base からクラスを派生させるだけでシングルトンクラスを作ることができます。シングルトンインスタンスはとくに明示しなくても自動的に作成されます。
こういった機構がない場合は、何かクラスをつくったら、そのインスタンスを生成する部分を、「どこか別の場所」に記述する必要があります。これだと何か機能を追加するたびにその「どこか別の場所」も修正をしなくてはならず、やっかいです。
Risaで用いているシングルトン管理では、どのモジュールがリンクされているかに従って自動的に初期化を行ってくれます。シングルトンインスタンスを生成したくなければ単にそのモジュールをリンクしなければよく、新しく機能を追加する場合は単にモジュールを書いてリンクすれば良いと言うことになります。
たとえば Timer クラスを実装しているモジュールは、Timer クラスを Risse のグローバルオブジェクトに登録します。Timer クラスを登録するためのシングルトンクラス (レジストラと呼んでいます) は Risse スクリプトエンジンのシングルトンクラスに依存していて、Risse が起動してから Timer クラスが Risse に登録されることを確実にしています。
Timer クラスを登録しているレジストラをリンクしなければ、Risa で Timer クラスは使用不可になります。同様の方法でクラスを書けば自動的に登録が行われるため、どこか一カ所に「グローバルオブジェクトに各クラスを登録」のような処理を書く必要がありません。
レジストラのようなクラスは他のソースファイルから見える必要はないため、ヘッダファイルにレジストラのシングルトンクラスの定義を書く必要はありません。
この機構を応用してオンデマンドのDLLやDSOの呼び出しに利用できないかと考え中。
シングルトンというのは簡単に言ってインスタンスが1つだけのクラスです。
boost には details::pool::singleton_default というシングルトンを実現するクラスがありますが、Risaの用途にはあわないし、boost に入ろうとしてたここのシングルトンクラス はレビューではねられていたようでその後どうなったかわからないのでとりあえず自分で作ってみました。
結構便利です。
シングルトンは「インスタンスは一つ」という所だけを取り出せばグローバルに置いたオブジェクトと変わりがないのですが、C++で普通にグローバルにオブジェクトを置いてしまうと、オブジェクトの生成と消滅の順序を保証することが出来ません。シングルトンとは言っても、シングルトン同士の間には依存関係を持っているものがありますし、シングルトンを使う立場にあるオブジェクトは、当然どれかのシングルトンに依存しています。シングルトンに依存している他のオブジェクトが死ぬ前にシングルトンが死んだりすると厄介です。そこで、寿命管理が必要になります。
boost の details::pool::singleton_default は main 関数の前にすべてのインスタンスの生成が終了し、main 関数の後にすべてのシングルトンが消滅することを保証できるのですが、Risa の シングルトンオブジェクトの多くは wxWidgets に依存しており、wxWidgets は main 関数の直後に初期化されて main 関数が終わる前に消滅するのでこれは使えませんでした。あとコンストラクタで投げた例外を捕捉しようがないのがつらいです。
Risaで使っているシングルトンクラスは (実装は ここのSingleton.* )、
class SomeSingletonClass :
public singleton_base<SomeSingletonClass>
{
public:
// ctorとdtorはprotectedでよい publicにする
SomeSingletonClass();//default ctor requied
~SomeSingletonClass();
public:
void SomeMethod();
};
で SomeSingletonClass をシングルトンにすることが出来ます。SomeSingletonClass::instance() でインスタンスにアクセスすることが出来ます。
このシングルトンが他のシングルトンに依存している場合は
class SomeSingletonClass :
public singleton_base<SomeSingletonClass>,
depends_on<OtherSingletonClass>
{
...
};
と書くと、SomeSingletonClass は OtherSingletonClass に依存していることを表すことができ、SomeSingletonClass のインスタンスが生成される前に OtherSingletonClass のインスタンスが生成されることを保証できます (消滅の順序はその逆になる)。
depends_on はほかのクラス(なんでもよい) に継承させてもよくて、
class MultitonClass : depends_on <SomeSingletonClass>
と記述した場合は MultitonClass のインスタンスが生存している期間、そのシングルトンクラスが生存することを保証します。なにかクラスを作って、そのクラスのインスタンスが特定のシングルトンに依存していることを表す場合に便利です。
singleton_manager::init_all() は、未初期化のシングルトンインスタンスの生成をすべて終わらせます。この際に発生した例外などは捕捉することができます。
class SomeSingletonClass : public singleton_base<SomeSingletonClass>, manual_start<SomeSingletonClass>
として、manual_start を継承させると、singleton_manager::init_all では初期化されないシングルトンを定義することができます。この場合は一番最初に SomeSingletonClass::instance() (あるいは戻り値のない SomeSingletonClass::ensure()) が実行された時点でインスタンスが生成されます。あまり使われないくせに生成に時間のかかるシングルトンはこれにします。
singleton_manager::disconnect_all() は、すべてのシングルトンインスタンスを消滅させます。が、シングルトンクラスに依存しているオブジェクトがまだ存在していた場合などは、この時点でシングルトンインスタンスが消滅する保証はありません。
寿命管理には内部的に boost::shared_ptr を使っているので、相互依存 (相互参照) とかやってしまうとエラいことになりますし、最初にインスタンスが生成される際の、複数スレッドからの多重アクセスからの保護がなかったりします。汎用的に他のアプリケーションでも用いる場合はもっと考慮しなきゃいけないことが多いのでその際は書き直したいと思いますが、とりあえず Risa の用途では問題がないので、これでいいやと考えています。