続 GIMP の中身


うひひ. 続ときたよ. 前回はタイルで終ってたので, せっかくだから今回はその続きといきますか.

タイルっす

タイルって何じゃいなというひとは居ないと思いますが, 念のため断っておくと, gimprc に出てくる tile-cache-size とかでいうタイルのことです.

GIMP では, 画像データ (レイヤとチャネルの総称) を内部的にはタイルという形で扱っている. 「タイル」の名前どおり, 画像をある大きさのタイルに分割して 一つの単位とし, メモリを割り当てたり描画したりしているのだ. サイズは 64x64 pixel である. このサイズで 1チャネルあたりのサイズは 4KBytes だ. たしかこれは i386版のページングサイズではないか? 違うかも.

要するに, アプリケーションが, 画像処理に適した形で メモリ管理を実装していると考えればいいだろう. メモリ管理といえばカーネルの仕事だが, それをカーネルに任せずにアプリケーションがわざわざやる, ということの意味はどこにあるのだろうか? その辺から今日は攻めていきたい. おお! なんかこのコーナーにしては極めて珍しく, ちゃんと コンピュータ サイエンスみたいな内容だぞ.

普通, アプリケーションのメモリ管理はカーネルがやる. アプリケーションの方でも, あるていどメモリがどうなっているか, 考えながら動いているわけだが, それに失敗してひとんちにメモリが はみ出したりしたときに, 「はーい. あんたはダメです. 死になさい」 みたいなことを言って交通整理しているのはカーネルである. また, 割り当てられたメモリが実はディスクの上だったりしても, アプリケーションは知ったことではない. 要らないページをディスクの上に出して, 必要なものをコアメモリの 上に置いておく, それはカーネルの仕事. これがメモリ管理.

メモリ管理は, スケジューリングなんかと同じように, システムの性能を決める非常に重要な仕事だから, そこんところのコードは極限まで洗練されている. 絶対に安全で, かつ, 目もくらむほどのスピードで処理されるように, 世界じゅうのハッカーが知恵を絞ってコードを書いたところだ. しかも, 処理は全てカーネル空間で行われるので, 非常に速い. わざわざそれを使わずに, ユーザ空間 (ps とか top とかで見えるアレ) でメモリ管理のまねごとをやっているのはなぜなのか?

答えは簡単だ. GIMP の作業では アドレス空間として非常に大きなものを要求され, しかもそれが極めて劇的に変化するからである. 通常の環境で使えるアドレス空間はどれくらいだろう? たとえば, 512MB のメモリをおごった最強のマシンを考えてみよう. デスクトップ用途なら, このスペックだと swap すると負けだから, 正味 512M がアドレス空間と仮定しよう.

もし, ユーザモードでメモリ管理を実装していなかったとしよう. 使えるメモリは正味 512MB だ. もっとも, 実際には X サーバや カーネルなんかが使うので, 全部というわけにはいかんがのう. で, 600dpi の 4チャネルの A4 画像を扱うことを考えよう. レイヤ一枚で 140MB のサイズになる. これじゃあ, レイヤがたった 2枚しか作れない.

GIMP がたった一人のユーザによって使われているという状態でコレだ. 他にも動かなければならないプログラムはいっぱいある. それに, 他の人だって GIMP や ImageMagick を使いたい. ノートパソコンだったりしたら, データを読み込むことさえできないかもしれないのだ. これじゃさみしすぎる. つまり, 画像処理プログラムが必要なメモリというのは, 通常, システムが用意しているアドレス空間では全然足りないわけです.

そこで, GIMP では画像データをタイルという単位に分割し, 使っていないタイルは ~/.gimp/ 以下のファイルに書き出すことで, システムが用意したアドレス空間とは独立に, 画像専用のアドレス空間をアプリケーションが独自に作ったのである. この機構を導入することによる効果は二つある. 一つは, ハードディスクの許す限り, そして, ファイルサイズの許すかぎり, でかいアドレス空間が使えることになることだ. 最近の環境なら 2G のファイルでパーティションが埋まるってことも 無いだろうから, Linux なら正味 2G (プラス tile-cache-size) の画像データを処理可能だ. 2G というと, A4 600dpi を14枚だ. ま, これを多いと思うか少ないと思うかは人によっていろいろだろうが, アドレス空間をアプリケーションが独自に用意する意味は, 少し明らかになったと思う.

タイル化にはもう一つの意味がある. それは メモリ利用効率の向上とデータ処理速度の向上だ. さっき要らないタイルをディスクに書き出すと言ったが, 要るものを手もとに置いておき, 要らないものをディスクに置いておくという操作, つまり, ここで「タイルのキャッシング」という話がでてくることになる.

キャッシングです

画像を全部いっぺんにメモリに読み込んで操作する場合, 馬鹿正直に大きな画像を扱うと 極端に処理速度が低下してしまう. たとえば, 画像の x軸と y軸方向の 2次元配列として処理する場合, 画像のほんの一部分だけに手を加えたり, 拡大して表示するにも, いちいち巨大な配列を処理せねばならない.

これに対して, 画像をタイルとして分割し, 処理に必要な部分だけを メインメモリに読み込んで処理するならば, そのような速度の低下を回避できることになる. このあたりは, どれくらいのサイズのデータを扱うか, で判断を せねばならないところだ. つまり, 比較的小さなサイズのデータならば, タイル化のオーバーヘッドが不利に働くし, そもそもプログラミングが 絶望的に(少なくともわしにとっては) 複雑になる. しかし, 何等かのキャッシング機構を備えていない場合は, プログラムがデータのサイズにスケールしない. GIMP のような 仕組みを持っていない画像関連プログラム(普通の画像ビュアとか) で大きな画像を読み込んだりすると, それを実感できる. ただし, GIMP の tile-cache-size が適切に設定されていれば, の話だが.

この, メインメモリに読み込む (キャッシュする) タイルのサイズ. これが gimprc で設定する tile-cache-size なのだ. tile-cache-size の効き具合は, たとえば, タイルの合計サイズがこの値に迫ってきて, メモリ内部で新しいタイルを確保できなくなったときには, 集中的な swap を開始する, てな具合である.

こんなふうに, タイルキャッシュ機構は最高にカッコイイですが, タイルのキャッシングを制御するための, 非常に低レベルなスペックを 直接ユーザが設定できる (せねばならない) という設計は, どっちかというと, かなりダメでしょう. システムのメモリの余り具合を見て, 適当に確保するような作りであってほしい ところじゃのう. ま, 「なりふり構わずメモリをふんだくる」から「遠慮がちモード」 みたいなのが選べるというのは悪くないかも.

tile-cache-size の選び方

とかいっててもしょうがないので, tile-cache-size の選びかたを, 上の話を元に考えてみよう.

もし, 値が小さすぎた場合, ちょっとデカい画をいじると 集中的にスワップが発生し, 全然使いものにならないだろう. デフォルトの tile-cache-size は 10M だ. (1.1.19 では 32M になる) 8枚レイヤで 560x560 にしかならん. 論外である. GIMP をインストールしたら, まずここの設定をもっとデカい 値に変更せねばならないのだ.

ところで, これが デカすぎた場合はどうなるだろうか. GIMP は tile-cache-size にタイルの合計が迫って来ると, 集中的に swap を始める. swap はいうまでもなく, GIMP がユーザ空間で行う 処理だ. ところで, cache されているタイルはどこにあるかとういと, これは system から割り当てられた仮想メモリ空間にあるわけだ. 当然, 仮想メモリなので, モノがコアメモリにあるのか, それとも実は swap されていてディスク上にあるのかは, アプリケーションの 知ったことではない. tile-cache-size がでかすぎた場合, cache が OS の用意した swap (swapon -a されるアレ) にはみ出している場合が考えられる. もし, cache しているタイルが system が用意した swap ファイルにあったとしよう. そして, GIMP がそのタイルを swap せねばならなくなったとしよう. どうなるだろう?

データは OS の swap から ~/.gimp/ 以下へ移動することになる. 当然, ディスクアクセスに加えてカーネルモードとユーザモードの コンテキストスイッチングが集中的に起き, 猛烈な性能の低下を招いてしまう. しかも, 1.0 系の場合, そこでガッキーンと処理が止まってしまい, 一歩も進まない. (1.1系なら, タイルの swap はメインのイベントループとは別のスレッドだから, 多少マシだ)

この値は デカすぎても小さすぎてもダメである. どっちかというと, 大きすぎる方がダメだね. でも, 1.1.x では swap は別スレッドだし, cpu やグラフィックに 負荷をかける仕事でもないので, 集中的な swap が起きても, 全然つかいものにならない, ってわけじゃあなくなっている. だから, 1.0 のときよりも tile-cache-size を大きくとっても大丈夫だ. 実際, バージョン 1.1 (そして 1.2) では, 1.0 に比べて大きなデータサイズを 扱ったときの処理能力, 操作感が格段に向上しているぞ. もし, マジで GIMP を使う気なら, もう, 1.0 なんか使ってちゃ絶対ダメだ! ところで, 具体的な設定だが, GIMP 最優先で考えるならば, tile-cache があふれるところで, ちょうどコアメモリが埋まるくらいのサイズが 適当ということですな.

わしの場合は, GIMP 本体や X サーバの使ってない部分, デスクトップ関係の アレコレ (GNOME とか), その他諸々のプログラムは全部ステ. swap されてしまっても構わない, と考えて, 実装メモリマイナス 20MB くらいの設定だ. つまり, この場合は 120M 載ってれば, そのうち 100M をGIMPのデータ領域として使うということだ. まさに GIMP 命の設定といえよう. それでもファイル共有やら smtp やら apache やらが コケることなく平然と動いてるから, Linux ってスゲエっすね. 魔法っすね. いや, マジで.

tile-cache-size が, GIMP の性能の死活を制する設定であることが判っていただけたと思う.

ところで, 現行の Linux-2.x で ext2 ファイルシステムの場合, swap ファイルが 2G になってしまったら新しいタイルが 全く作れなくなるわけだ. その場合, sigabort が発生し, それを拾った GIMP が「タイルが作れません」みたいなメッセージを出すようになっている. なっているんだが, 何か知らんがその後断りもなく死んじまう (バージョン 1.1.19)ってのは 最近のバージョンではどうなってんのかね?

1.2 の良いところ

ちょっと書いたが, バージョン 1.2 では GIMP もマルチスレッドになる. いろんなところでこれが使い勝手の向上に, ぐぐっと効いているのだが, キャッシングの性能向上もマルチスレッド化とアルゴリズムの改善のおかげで 著しいものがある.

1.2 では swap が終るまで待っている必要はない. swap は 別のスレッドで行われるからだ. ただ, 必要なタイルが 確保できないと描画関数が発行できないから, intensive swap が 大規模に発生した場合は待たされる場合もあるが, ツールの切替えなどの イベント処理はタイルと関係ないので, intensive swap 中でも常に可能である.

キャッシングのアルゴリズムは, 以前のようにメモリが足りなくなってからガリガリ 鳴り始めるのではなく, 苦しくなるまえにバックグラウンドで 密かに swap が始まり, そして, 実際に tile-cache-size に タイルのサイズが到達してから intensive swap が開始されるという, 2段がまえになった. できるだけ intensive swap が発生しないように なっているわけだ. これも, わしみたいに 56MBytes という乏しいメモリで 作業しているせつない人には非常に嬉しい改善だ.

こういった改善により, 一時期 最低の安定性だったタイル関連だが, 最近は ほとんどタイル絡みで落ちることもない. 大きなデータを扱ったときの操作性が向上し, 非常に快適に使えるように 進化したのである. すげえっすね! ちなみにタイル関連でこけると, いきなりコアプログラムが, すどーん, と即死だ.

タイルの負の遺産と バージョン 2.0 への道

このように, 若干難儀なところはあるものの, ユーザモードで画像処理に特化したメモリ管理機構を備えている GIMP は, それ自体なんつうか非常に高度でイカスプログラムであるとも 言える. しかし, 残念ながら, これが良いことばかりではないようだ.

現在, GIMP の抱えているもっとも重大な問題の一つは, チャネルあたりの データサイズが 8bit しかない, ということだと言われている (Who says so?). これを拡張しようとするのが猛烈に大変な仕事になってしまっているのも, なんとなく知っている人も居るかもね. これは, 単にチャネルあたりのデータサイズを増やすとか, そういう問題ではなく, CMYK モデルをもげ, とかそういうところにも 関係している. タイルは RGBプラスアルファチャネル, 1チャネルあたり 8bit ということで, タイルを共有メモリから受け取るプラグインも, 直接操作する描画関数も, そういう作りになっているもんですから. いやー. これがマジで大変なんですわ.

これが, タイルをもうちょっと抽象化したオブジェクト (仮に, ここでは GIMP16 プロジェクトに敬意を表して それをキャンバスと呼ぼう) への操作として実装されていたならば, タイルの実装を変更したとしても 今のように全部書き直しということにはならなかったわけで.

8bit を 16bit に拡張するときは, 今の 8bit 依存なコーディングから 16bit 依存なコーディングに引っ越すのではなく, タイルとユーザエンドの関数の間に一枚挟んだ設計に改めるとかなんとかかんとか, やあもう, ちょっと私の把握できる世界ではございませんが, そういう話が あったりなかったりするようですね. つまり, 2.0 への道は, メモリ管理という意味でいえば, かなり大変かも. まじで. ほとんど全部書き直しかも.