先日の「ゼロ幅マッチ時に検索開始位置を1文字進める処理」を漢字やサロゲートペアに対応させていなかったことに気付いた。後で修正しよう。
bregonig.dll の \G の動作を Bregexp.dll と同じにするため、Oniguruma を改造。\G がマッチすべき位置を直接指定できるようにしてみた。(onig-5.9.0-mod.diff)
どうやら \G の動作が Bregexp.dll と同じになったようだ。
Bregexp.dll での \G の動作を調べてみることにした。Bregexp.dll の仕様を見ると、\G は対応していないと書かれている。しかし、ソースを見ると \G に関係すると思われるコードがある。実際に動かして確認してみた。
BMatch() に m/\G../g
というパターンを与えてみた。Perl では、同じ文字列に対して何度もこのパターンを適用すると、マッチ位置が2文字ずつ後ろにずれていくが、Bregexp.dll では何度繰り返しても文字列の先頭2文字にしかマッチしない。
次に BSubst() に s/\Gx?/!/g
というパターンを与え、"abcde" に対して置換を実行してみた結果、"!abcde" が得られた。他の場合もいくつか試してみた。
"abcde" =~ s/x?/!/g; # "!a!b!c!d!e!" "abcde" =~ s/\Gx?/!/g; # "!abcde" "abcde" =~ s/\G(.)/!$1/g; # "!a!b!c!d!e"
関数の呼び出しをまたがった \G の位置の保存はされない。そのため、BMatch() では、\G は常に検索開始位置(=文字列の先頭)にマッチする。
BSubst() で s///g
を使った場合は少々複雑である。BMatch() と同様に、関数の呼び出しをまたがった \G の位置の保存はされないので、最初の置換では \G は必ず検索開始位置(=文字列の先頭)にマッチする。1回の呼び出しで、置換が複数回行われる場合、2回目以降の置換では、通常、前回のマッチ終了位置から検索を開始するので、\G は内部的な検索開始位置にマッチする。しかし、前回のマッチがゼロ幅マッチだった場合は、無限ループを避けるため、内部的な検索開始位置は強制的に1文字進められる。この場合、前回のマッチ終了位置と内部的な検索開始位置が一致しなくなり、\G も内部的な検索開始位置にはマッチしなくなる。
\G が Perl のように前回のマッチ終了位置を意味する場合と、鬼車のように照合開始位置を意味する場合で動作が異なるのは、ゼロ幅マッチの場合だけなのだろうか?
何が何だかよく分からなくなってきた。また後で再度詳しく調べよう。
久しぶりに Project-fao を見に行ったらアクセスできなくなっていた。幸い、Internet Archive には、「CD-ROM/DVD-ROMの制御法」など、以前の記事が残っていた。
ちなみに、以前書いたように、非公開 API を使って Win32 アプリから 16bit DLL を介さずに直接 MSCDEX を呼び出すという方法もある。サンプルを公開しようかと思いつつ、ずっとそのままになっている。需要はまず無いだろうが。
Internet Archive に保存された日本語のサイトを Firefox で見ようとすると高い確率で文字化けが起こる。Internet Archive のサーバーが HTTP ヘッダ内で
Content-Type: text/html; charset=UTF-8
というように、文字コードを UTF-8 と指定しているのが原因である。Firefox は、HTTP ヘッダで文字コードが指定されていると、ファイル内で指定されている文字コードよりも、そちらの文字コードの方を優先し、文字コードの自動判別も行われない。Internet Archive が間違った文字コードを指定しているのが悪いのだが、Firefox でも HTTP ヘッダの文字コード指定を無視するような設定はないのだろうか。
「Firefox 3で実現されるアニメーションの世界 - APNG、JavaScript+Canvas」という記事を見た。APNG (Animated PNG) という名前は初めて聞いた。MNG とはどう違うのだろう・・・ということで調べてみた。「APNG・これまでの経緯」に詳しく書いてあった。MNG の仕様は複雑すぎるということで、PNG をベースに Mozilla 側が新たに作ったのが APNG だそうだ。Mozilla 側は APNG を PNG の仕様に取り込んでもらおうとしたが、PNG 開発側は PNG は1枚絵の規格であるという哲学にこだわり、拒否。Mozilla 側はそのまま APNG の開発を継続とのこと。
touch, dirtime.dll スレッドセーフ化。springm.dll や、Win32 アプリから直接 MSCDEX を呼び出すサンプル(非公開)も同じコードを使用している。
SpringM 1.50k29 にバグ発覚。ディレクトリ名に2個連続した半角スペースが含まれていると、そのディレクトリではファイルの関連付け実行やプログラムの起動ができない。オリジナル版から存在するバグかどうかの確認はまだしていない。原因調査や修正は当分先の予定。
FastFile で取得できる属性だが、実は、NTFS に対して実行すると、canRead(), canWrite(), canExecute() が正しい値を返さない可能性がある。NTFS では ACL を使って、ユーザーごとに、ファイルの読み取り、書き込み、実行許可を設定できる。(参考:アクセス制御リストACLとは?)
しかし、すでに書いているように FastFile は、FindFirstFile()/FindNextFile() を使ってファイルの一覧と属性を取得しているが、この API で取得できる属性はグローバルな属性であって、ACL を反映したものではない(はず)。現在の FastFile では、canRead(), canExecute() は常に、true を返し、canWrite() は WIN32_FIND_DATA.dwFileAttributes の FILE_ATTRIBUTE_READONLY ビットに応じた値を返すようになっている。
今後もこの仕様のままとするか、それとも canRead(), canWrite(), canExecute() の返す値は、FastFile.listFiles() では設定せず、初回呼び出し時に File.canXxxx() の値をキャッシュするようにするか、あるいはそれ以外の仕様にするか検討が必要である。ただ、FastFile.listFiles() で ACL を読むのはコストが高そうなので、あまりやりたくない。
ディレクトリの内容をすべて WIN32_FIND_DATA の配列としてメモリに読み込んでから、最後に Java の配列に変換するという現在の動作はメモリの使用量という点で明らかに問題がある。なぜこのような実装にしたのか、原型を作ったのは6年も前のことなのでよく覚えていない。C++ からの Java コードの呼び出しは非常に遅いので、その呼び出し回数をできるだけ減らすためだったような気もするが、配列の要素ごとにコンストラクタを呼び出さないといけない点はどうにもならない。(どうにかしようとして、あがいた結果の残骸だったか?)
WIN32_FIND_DATA を1つ読み込むごとに Java のオブジェクトに変換するように変更したところ、30k個のファイルの属性取得に掛かる時間が 0.4秒から 0.3秒弱に短縮された。File.listFiles() によるファイル一覧取得に掛かる時間と同程度か、場合によってはそれよりも短い時間でファイル一覧と属性の取得ができるようになってしまった。
改良した FastFile を Win98SE 上で実行してみたところ、JavaVM が落ちてしまった。以前の FastFile を調べてみると、Ver.1.20 も落ちてしまった。Java のバージョンが関係あるかと思ったが、1.4, 5, 6 のいずれでも落ちてしまった。落ちている箇所は Double.toString() らしい。いろいろ調べてみたが分からずいったん諦めた。
unicows.dll には浮動小数点関連のバグがあるという話を思い出した。「"Microsoft Layer for Unicode"のバグ疑惑」に詳しく書かれているが、_control87() で設定できる浮動小数点制御ワードの値が勝手に書き換わってしまい、浮動小数点例外の扱いが変わってしまうらしい。
MSLU と同等の処理を自前でやるように変更したところ、無事正常に動作するようになった。一部、処理がスレッドセーフになっているか不安なところがあるが、おそらく大丈夫だろう。たぶん・・・。
unicows.dll の代わりに Opencow を使っても良かったのかもしれないが、確認はしなかった。
上記の FastFile の問題が Java のバージョンに関係するかを確かめるために、Java 6 SDK Update 2 を Win98SE にインストールしてみた。Win9x へのインストールは推奨しないとの警告を無視してインストールしたら、JRE のインストールの段階でエラーが発生してしまった。インストールに失敗したので、ロールバックが行われたのだが、その途中に、ハングアップしてしまった。再起動したところ、JDK だけはインストールされていた。面倒だったので、この中途半端な状態で動作確認を行ってみた。
FastFile の速度について、ファイル数に比例していないとの指摘があった。自分でも 30000個のファイルを作って確かめてみたが、確かに同じような結果になった。ちなみに、30000個のファイルを作るには、例えば次のようなコマンドを実行すればよい。
for /l %i in (1,1,30000) do @copy nul %i.txt>nul
調査の結果、HeapReAlloc() を使って巨大なメモリ領域の拡張を何度も行っているのが遅くなる原因だった。HeapReAlloc() でメモリ領域の拡張を行う際、現在の領域の直後に空きがない場合は、新たな領域が確保されて内容のコピーが行われるが、元の領域が大きければ当然大量のメモリコピーが行われることになる。
現在の FastFile の実装では、FindFirstFile()/FindNextFile() を使ってファイルの一覧と属性を取得し、結果をすべて WIN32_FIND_DATAW の配列としてメモリ上に読み込んでから、Java の配列に変換している。WIN32_FIND_DATAW のサイズは 592bytes なので、30k個のファイルの情報をロードするには 18MB 近いメモリが必要になる。現在の実装では、1024個ずつ領域を拡大しているので 29回の領域拡張が行われ、最悪 250MB ものメモリコピーが行われる可能性がある。
メモリ領域を倍々に増やすように変更したところ、30k個のファイルの属性取得に 1.0秒掛かっていたものが、0.4秒程度に短縮された。しかし、まだ改善の余地あり。
ここ数年 Java コードは全く書いていなかったのですっかり浦島太郎状態。FastFile を JDK 1.6 で javac -Xlint:all としてコンパイルしたところ、次のような警告が出た。
警告:[unchecked] raw 型 java.util.ArrayList のメンバとしての <T>toArray(T[]) への無検査呼び出しです。 警告:[serial] 直列化可能なクラス jp.ne.anet.kentkt.fastfile.CachedFile には、serialVersionUID が定義されていません。
1つ目の警告は、1.5 で導入されたジェネリックスに関わる警告。ジェネリックスを使うように書き直そうかとも思ったが、そうすると 1.4 では動作しなくなってしまう。-source 1.5 -target 1.4 でコンパイルできるといいのだが。
2つ目の警告は・・・よく分からないので無視しよう。
掲示板にも書いたが、bregonig.dll の BSubst() で \G を使うと動作がおかしくなる。よく調べたところ、マッチ長が 0 のときだけではなく、そもそも、s///g で \G を使うと Perl とは違う動作になってしまう。まさに「ゼロ幅マッチの問題を片付けたいが」に書かれている落とし穴にはまっていた。さらに、Perl と鬼車では \G の意味が異なっていることも原因の一つと考えられる。これに対処するには、bregonig.dll 側だけでは無理で、鬼車の改造が必要と思われる。\G がマッチする位置を明示的に指定できるようにすればとりあえずは良さそうな気がするが、さてどうしよう。
先月の spam メールの集計結果。着信拒否にならなかった spam が少なくとも 2284通。そのうち、@nifty の迷惑メールフォルダーでも Spam Mail Killer でも spam として認識できなかったものは 8通(0.4%)。今月は @nifty の迷惑メールフォルダーの判別ミスが少し多かった。