From JCA02266 @ nifty.ne.jp Sun Jan 5 13:05:11 2003 From: JCA02266 @ nifty.ne.jp (Koji Arai) Date: Sun, 05 Jan 2003 13:05:11 +0900 (JST) Subject: [Lha-users] The Hacking of LHa for UNIX (1st draft) Message-ID: <20030105.130511.59492744.JCA02266@nifty.ne.jp> 新井です。 突然ですが The Hacking of LHa for UNIX (1st draft) ------------------------------------------- 2003-01-05 Koji Arai 本書は、LHa for UNIX 1.14i のソースを解読し、その圧縮アルゴリズムの実 装を確認するためのものだ。この成果は別の形でまとめなおされ資料になるか もしれないし、このままの形で保管されるかもしれないし、新たにソースを書 き起こす元になるかもしれない。とにかく、暇な正月休みを潰すためにやって みただけのものだ。(休みが明けるとまた忙しくなるので、これ以上まったく 何もしないかもしれない) 現時点では、slide.c の解析だけである。huf.c も同時進行で解析中だが、文 体が異なる(読みものとしての体裁を整えていない)ので公開するとしても別文 書になるだろう(huf.c の解析文書は現時点でデバッガによるおっかけが中心 のメモでしかない) # 本当は、huf.c の解析を先に行っていた slide 辞書の encoding 部はなく # ても LHA 互換アーカイバは作れそうだったからだが、huf.c は slide.c # に比べて難解な部分が多そうだから、後回しにした。 本書は、まだ未完成である。にもかかわらず公開するのはこれ以上続かないか もしれないからである(気が向いたらまた続きを書くだろう。あるいは応援の お手紙がくればやる気が出るかもしれない)。 本書はフリーである。複製、改変、再配布は自由であるということだ。ただし 本書により生じたあらゆる損害、不利益に対しては一切の保証はない。本書に は嘘があるかもしれない。それに対して嘘を教えられたと著者を避難をしない で頂きたい。しかし間違いの指摘は構わない(ぜひお願いしたい)、著者は圧縮 処理に関しては無知である。用語の使い方等は適切でないかもしれないのでこ の方面でも御指導頂ければ幸いである。 =============================================================================== o 表記について * 関数は、その定義ソースfile.c と関数名 func() を示すのに file.c:func() という記述を使う * o 用語について * 符号 符号化、符号語、圧縮文 * 復号 復号化、復号語、復号文 =============================================================================== まず、構造について考える。 slide辞書法は、encoding にさまざまな工夫が凝らされるのでとても複雑だが、 普通 decoding は単純である。decoding を解析することでどのような圧縮文 を作っているのかを調べてみる。 decoding を行う処理は、slide.c の decode() である。この処理を見てみる と思った通り簡単に解読できた(以下) 1. huffman coding により復号した文字を環状バッファ dtext に書き込む 通常の文字 c は、c < 256 で表現されている(つまりそのまま) 2. 通常の文字でないものが現れたら、それは長さである。長さ len は、 len > 255 で表現されている。len から 0xfd(253) を引いた値が 実際の長さを表す(-lzs- method の場合は、0xfe(254)を引く) 「長さ」が、現れたらその直後には「位置」が書かれているのでそれを 読む。こうして、長さと位置のペアを得る dtext から pt+1 バイト前の len バイトを読み、dtext に追加で書き込む 3. dtext が一杯(dicsiz)になったらファイルに書き出す これの繰り返しである。つまり、slide 辞書法の圧縮文は、文字 c と の並びであることがわかる。例えば、文字列 c1 c2 c1 c2 は、以下のよ うに表現されているはずである。(本当は、長さが 2 以下では圧縮が起こらな いので平文のまま出力する。長さは最低でも 3 は必要) +----+----+--------+ | c1 | c2 | <2, 1> | +----+----+--------+ では、この構造を作成する圧縮処理について考える。slide 辞書法では、ファ イルから読み込んだ文字列 token が、以前に読み込んだ辞書に存在すれば のペアを出力し、存在しなければ token をそのまま出力する。読 み込んだ token は、辞書に追加し、辞書の語が溢れたら古い情報を捨てる。 何も予備知識がない状態で書けば while (read_file(&token, tokensiz)) { len = search_dict(dict, token, &pt); if (len == -1) { print_token(token); else print_pair(len, pt); update_dict(dict, token); } のようになるはず。ここで、tokensiz は token の最大サイズで、最長一致長 を表す。この値が大きければ大きい程、圧縮効率は良くなるはずで、lha では、 これは MAXMATCH{256}である。また、dict は辞書でこのサイズは lha の -lz5- メソッドでは、8192 となっている。この辞書も大きければ大きい程良 いはずだ。その方が一致文字列が見つかりやすい。(ただし、辞書が大きいと 一致位置を示す情報 の情報量が増えるはずだし、速度も遅くなる だろう。後で検証する) で、実際にソースを見てみると(slide.c:encode())・・・、まったくこのよう な構造にはなってないように見える。何やらややこしいことばかりでまったく わからない。なぜここまでややこしいのかと泣きたくなってくるが、それは速 度のためである(本当?)。上記のコードで、search_dict() は、単に dict か ら token に一致する位置を検索するだけで良さそう(実際にそれでも良い)だ が、これではまったく速度が出ない。このあたりの工夫が slide 辞書法のキ モである。 そういうわけで、この部分を読み解くことにする。なお、予備知識として lha では、辞書から token を探すのにハッシュが使われているらしいことを記し ておく。 ここでは実際にデバッガで動作させながら解読するのではなく、ソースを読む だけで理解できるかを試すことにする。また、本文は某書(謎)のノリをマネて いると指摘する方がいるかもしれない・・・がまったくその通りだ。 まず、そのものずばりの encode() (slide.c) を見る。以下がこの関数だが 処理の要点だけに着目するために不要そうな部分は(現時点で予測で)削った。 unsigned int encode() { int lastmatchlen; unsigned int lastmatchoffset; /* (A) */ init_slide(); /* (B) */ remainder = fread_crc(&text[dicsiz], txtsiz-dicsiz, infile); encoded_origsize = remainder; matchlen = THRESHOLD - 1; pos = dicsiz; if (matchlen > remainder) matchlen = remainder; /* (C) */ hval = ((((text[dicsiz] << 5) ^ text[dicsiz + 1]) << 5) ^ text[dicsiz + 2]) & (unsigned)(HSHSIZ - 1); /* (D) */ insert(); while (remainder > 0 && ! unpackable) { /* (E) */ lastmatchlen = matchlen; lastmatchoffset = pos - matchpos - 1; --matchlen; /* (F) */ /* (G) */ get_next(); match_insert(); if (matchlen > remainder) matchlen = remainder; /* (H) */ if (matchlen > lastmatchlen || lastmatchlen < THRESHOLD) { /* (H.1) */ encode_set.output(text[pos - 1], 0); count++; } else { /* (H.2) */ encode_set.output(lastmatchlen + (UCHAR_MAX + 1 - THRESHOLD), (lastmatchoffset) & (dicsiz-1) ); --lastmatchlen; while (--lastmatchlen > 0) { get_next(); insert(); count++; } get_next(); matchlen = THRESHOLD - 1; match_insert(); if (matchlen > remainder) matchlen = remainder; } } } まず、この関数から概観を見てみると、ループの前に初期化処理として 以下が行われている。 (A) init_slide() 初期化する (B) ファイルを読み込み text[] に格納する。 (C) ハッシュ値 hval を計算する。 (D) insert() する (きっと辞書に token を追加しているのだろう) そして、ループ処理では以下の処理が行われている (E) lastmatchlen, lastmatchoffset, matchlen を更新する。 (F) get_next() (次の token を読む。たぶん) (G) match_insert() (辞書に追加する。たぶん) (H) matchlen > lastmatchlen || lastmatchlen < THRESHOLD なら (H.1) output() する。(マッチしなかったらそのまま出力しているのだろう。たぶん) (H.2) そうでなければ(マッチしたなら)、output()し、何かいろいろする。 現時点で、(H.2) の部分はよく解読できなかった。何やら再度 get_next() が 呼ばれていたりして思った通りの処理フローにはなっていない。だが、ここで は焦らず放置することにして、ここまで予想で書いた部分の細部に触れること にする(単にここまでの予想が間違っているだけかもしれないのだから、わか らない部分を無理にわかるように頑張る必要はなかろう) 関数の細部に触れる前にデータ構造について調べておく。データ構造に対して の理解が深まればアルゴリズムの80%は分かったも同然だ(誇張)。slide.c で 使用されているデータ構造は以下の通りだ。(不要そうだと思うものは除いて ある) static unsigned int *hash; static unsigned int *prev; unsigned char *too_flag; static unsigned int txtsiz; static unsigned long dicsiz; static unsigned int hval; static int matchlen; static unsigned int matchpos; static unsigned int pos; static unsigned int remainder; too_flag だけ、static がついてないが、他のソースを grep してもこの変数 を使っている箇所はない、単に static の付け忘れだろう。 これらの変数は、encode() の冒頭 init_slide() で初期化されている・・の かと思ったら違った。slide.c:encode_alloc() で行われている。 int encode_alloc(method) int method; { if (method == LZHUFF1_METHOD_NUM) { /* Changed N.Watazaki */ encode_set = encode_define[0]; maxmatch = 60; dicbit = 12; /* 12 Changed N.Watazaki */ } else { /* method LH4(12),LH5(13),LH6(15) */ encode_set = encode_define[1]; maxmatch = MAXMATCH; if (method == LZHUFF7_METHOD_NUM) dicbit = MAX_DICBIT; /* 16 bits */ else if (method == LZHUFF6_METHOD_NUM) dicbit = MAX_DICBIT-1; /* 15 bits */ else /* LH5 LH4 is not used */ dicbit = MAX_DICBIT - 3; /* 13 bits */ } dicsiz = (((unsigned long)1) << dicbit); txtsiz = dicsiz*2+maxmatch; if (hash) return method; if (alloc_buf() == NULL) exit(207); /* I don't know this 207. */ hash = (unsigned int*)malloc(HSHSIZ * sizeof(unsigned int)); prev = (unsigned int*)malloc(DICSIZ * sizeof(unsigned int)); text = (unsigned char*)malloc(TXTSIZ); too_flag = (unsigned char*)malloc(HSHSIZ); if (hash == NULL || prev == NULL || text == NULL || too_flag == NULL) exit(207); return method; } 引数に渡された method (これは、lh1, lh5, lh6, lh7 などを示す)によって、 初期化される内容が変わる(encode_alloc()前半部分)。このことから各変数の 用途もわかる。 method maxmatch dicbit ---------------------------- -lh1- 60 12 -lh5- 256 13 -lh6- 256 15 -lh7- 256 16 ということらしい。dicbit というのは辞書サイズのbitサイズで、辞書サイズ は 2^dicbit で表されている。lh5 が 8KB(2^13)、lh6 が 32KB(2^15)、lh7 が 64KB(2^16) の辞書サイズを利用すると言うのは予備知識である。maxmatch というのは、token の最長一致長である。このことも予備知識として詳細には 触れない。(ところで、本書では当面、lh5, 6, 7 のことしか言及しない) encode_set, encode_define というのがあるが、method によって、Huffman coding の方法を変えていることはちょっと見ればすぐにわかるし、大したこ とではない。以降無視する。 encode_alloc() の後半では、他の変数の初期化(バッファの割り当て)が行われる。 dicsiz = (((unsigned long)1) << dicbit); dicsiz はそのものずばり辞書サイズである。 txtsiz = dicsiz*2+maxmatch; 現時点で txtsiz が何なのかはわからない。 if (hash) return method; hash はこの直後で割り当てられる。つまり、一度割当を行ったら、 encode_alloc() は、以降メモリの割当を行わない。ただそれだけだ。 if (alloc_buf() == NULL) exit(207); /* I don't know this 207. */ alloc_buf() は、huf.c で定義された関数。このことから Huffman coding の ためのバッファを割り当てているのだろう。ここでは無視。(しかし、207 と いうのは何なのだろう?) hash = (unsigned int*)malloc(HSHSIZ * sizeof(unsigned int)); prev = (unsigned int*)malloc(DICSIZ * sizeof(unsigned int)); text = (unsigned char*)malloc(TXTSIZ); too_flag = (unsigned char*)malloc(HSHSIZ); if (hash == NULL || prev == NULL || text == NULL || too_flag == NULL) exit(207); hash は、ハッシュ用の何か、HSHSIZ は、固定値で 2^15 である。 prev は、DICSIZから辞書だろう。要素の型が char でなく int であることに も注目しておく。DICSIZ は dicsiz でも構わないはず。単に「大は小を兼ね る」を実践しているだけであろう、TXTSIZ も同様である。おそらく、一度の 実行で複数の圧縮メソッドを使用した場合、そのメソッド毎に領域を割り当て るよりは最大の値をあらかじめ一度だけ割り当てた方が良いと考えたのだろう。 しかし、ソースを参照するときは繁雑になるので以降、 DICSIZ == dicsiz TXTSIZ == txtsiz であるとする。これ重要。 text は、現時点では不明 too_flag も不明 っとなる。まだ、良く分からないが、以下の図を書いておこう。後で何度も見 ることになるだろう。この図はスケールが lh7 の場合を想定しているが。こ のことは大したことではないはずだ。また、too_flag と hash のスケールが 一緒だがこれはサイズ(領域のバイト数)が一緒なのではなく、要素数が一緒で あることを示している。ほとんどの場合要素の型の違いというのは処理内容に とって重要なことではないはずだ。 ---------------------------------------------------------------------------- 0 2^15=32768 +-------------+ hash | | +-------------+ dicsiz=2^dicbit +-------------+-------------+ 2*2^dicbit prev | | | | +-------------+-------------+ v txtsiz +-------------+-------------+-------------+-------------+---+ text | | | | | | +-------------+-------------+-------------+-------------+---+ <---> maxmatch{256} too_flag 2^15 +-------------+ | | +-------------+ ---------------------------------------------------------------------------- 先に示した変数の中でまだ初期化には現れていないものがある。列挙すると static unsigned int hval; static int matchlen; static unsigned int matchpos; static unsigned int pos; static unsigned int remainder; だ、ざっとソースを眺めると slide.c:insert() という関数に hash[hval] = pos; というのが現れているから、hval は、hash[] の位置を指し、hash には、pos を格納すると推測される。同様に prev[pos & (dicsiz - 1)] = hash[hval]; というのも現れているから pos は、prev[] の位置を指し、prev には、 hash[hval] つまり、pos を格納しているようだ。これは少し謎な処理だが、 insert() の全貌は短い(というかこれだけ)なので、ちょっと横道にそれて詳 細に見てみよう。(現在の解析の趣旨は、変数の用途の概要を予想すること) /* 現在の文字列をチェーンに追加する */ static void insert() { prev[pos & (dicsiz - 1)] = hash[hval]; hash[hval] = pos; } コメントはまったく意味不明だが、無視して処理内容に着目する。prev[] の インデックス pos & (dicsiz - 1) は、dicsiz が 2^n であることからdicsiz はビットマスクであることがわかる。例えば仮に dicsiz が 2^8 だと dicsiz - 1 は、 8 7 6 5 4 3 2 1 0 bit -------------------------- dicsiz 1 0 0 0 0 0 0 0 0 dicsiz-1 1 1 1 1 1 1 1 1 である。このすべて 1 が立ったビットマスクと pos を & すると、どのよう な pos の値に対しても pos & (dicsiz - 1) は、prev[] のインデックスの範 囲に納まる。もう少し言うと pos が仮にインデックスの最大値+1だった場合、 pos & (dicsiz - 1) は、0 になる。これにより次の予想が立つ。 o pos が、prev[] の位置を指すのではなく、pos & (dicsiz - 1) が prev[]の位置を指す。(pos は、このインデックスの範囲を越える可能性がある) o それに反して、prev[] は環状バッファらしいという予想が立てばやはり pos は、prev のインデックスである。 prev が環状バッファであると予想が付けば話が早い。pos & (dicsiz-1) は、 pos と同義だと解釈可能だからである(prev が環状バッファでなく無限長のバッ ファであると想像しよう)そして、pos & (dicsiz-1) を pos に置き換えて、 再度処理内容に着目すると prev[pos] = hash[hval]; hash[hval] = pos; ということから、 1. (この関数に来る前に) pos が更新される。(予想) 2. prev[pos] に以前の hash[hval] (以前の pos)を格納する 3. hash[hval] に新しい pos を書く。 といった処理であることが予想される。コメントの「チェーン」もなんとなく 納得できる。新たな事実(まだ予想だが)が分かったので、図に書き記そう。 ---------------------------------------------------------------------------- 0 2^15=32768 +-+---+-------+ hash | |pos|... | +-+---+-------+ `-hval +----+----+-------------------- prev | |ppos| . . . +----+----+-------------------- `- pos * hash の取り得る値は pos その位置は hval * ppos は以前の pos を示す。prev は無限長のバッファ(本当は環状バッファ) ---------------------------------------------------------------------------- まだ、解析できてない変数が残っている。 static int matchlen; static unsigned int matchpos; static unsigned int remainder; しかしこれらはどうにもパッと見ではわからない。処理内容を追いかけないと だめそうだ。仕方ないので変数名で予想しよう。(幸い前の変数名と違って予 想しやすい)以下 ---------------------------------------------------------------------------- * matchlen 一致した文字列長 * matchpos 一致した辞書上の位置 * remainder token の残りサイズ ---------------------------------------------------------------------------- はたして、予想はあっているのか、今はまだ分からない。 slide.c を見る限りデータ構造は網羅できた。結局分かったのか分からないの か良く分からないが少しずつでも前進はしているはずだ。ここで、再度 encode() の処理を追いかけよう。今度は細部にも着目する。 前に、encode() のソースには (A) 〜 (H) までの記号を記した。この順番に 解析を進めよう。 /* (A) */ init_slide(); まあ、初期化である。内容を見てみると for (i = 0; i < HSHSIZ; i++) { hash[i] = NIL; too_flag[i] = 0; } だけである。NIL というのは、0 であると slide.c で定義されている。普通 このような初期値は、通常の値が取り得ない値を示している。NIL が 0 なら hash[] に格納される pos は 0 にならない可能性がある。まあ、予想ばかり 書いても仕方ないので、この関数は終ろう。余談だが、nil は null と同じで。 「ない」の意味だが、NULL がC言語ではポインタだから。別のマクロ名にした のかも知れない。いずれにしてもこの程度はマクロにする必要もなかろうとは 思うのは、余計なお世話かもしれない。 /* (B) */ remainder = fread_crc(&text[dicsiz], txtsiz-dicsiz, infile); encoded_origsize = remainder; matchlen = THRESHOLD - 1; pos = dicsiz; if (matchlen > remainder) matchlen = remainder; ファイルを読み込み、各変数の初期値を設定している。注目すべきはファイル を読み込んだバッファの位置である。fread_crc() は、crcio.c で定義された 汎用関数で、CRC値を計算したり漢字コード変換をしたりを除けば、fread() と同じである。つまり、ファイルは最初、 &text[dicsiz] の位置に、txtsiz-dicsiz 分だけ読まれる。 ことを示す。図示しよう。 ---------------------------------------------------------------------------- < 初期状態 > dicsiz=2^dicbit 2*2^dicbit v v txtsiz +-------------+-------------+-------------+-------------+---+ text | | | | | | +-------------+-------------+-------------+-------------+---+ `-pos <---> maxmatch{256} <------ remainder --------------> |--- この位置に最初の ---------| データが読まれている ---------------------------------------------------------------------------- ますます、text[] の用途が不明だが、slide 辞書法の典型的な読み込み処理 のことを考えるとある程度予想がつく(それを先に示した方が良いか?)。まあ、 ここではフーンっと鼻で納得して済まそう。 fread_crc() は、読み込んだバッファ長を返す。remainder がその値で、既に 図示してある。encoded_origsize は、ソースを見ると、元のファイルのサイ ズを表すためだけの変数のようだ。以降は無視しよう。 ところで、ファイルサイズが小さい場合図の通りにならないっと考えるかも知 れない。その通りなのだが、例外条件は少ない方がソースは理解しやすい。単 純な場合だけを考えた方が、あれこれ考えをめぐらす必要がないからだ。なに しろ既に動くソースを見ているのだから、細かいことに目をつぶってもエンバ グすることはないのである。そういうわけで、当面はこの図が唯一の初期状態 であると考える。 (B) の部分はもう少し注目すべき箇所がある。 matchlen = THRESHOLD - 1; matchlen は、「一致した文字列長」であると予想したが THRESHOLD の値は 3 (固定値)であるから、matchlen の初期値は 2 だ。いきなり予想がはずれた気 がする。予想を立て直そう。2 という謎な数値と match*len* について考える と、冒頭で のペアの len は 2 であることはないと説明した。無 意味だからであるが、matchlen の初期値はこの 2 と関連するかもしれない。 そこで、matchlen の用途を以下のように予想しなおすことにする。以下のよ うにメモを更新しよう。THRESHOLD(threshold は閾値の意)も一緒に予想した。 ---------------------------------------------------------------------------- * matchlen 最低限一致しなければならない長さ-1 * THRESHOLD 最低限一致しなければならない長さ ---------------------------------------------------------------------------- うーん、本当だろうか? (B) の残りの部分を片付けよう pos = dicsiz; if (matchlen > remainder) matchlen = remainder; pos が dicsiz であることからどうやら、pos は、text[] のインデックスら しい。前の予想で pos は、prev[] のインデックスでもあり、hash[] の値で もあると予想したのだが(これはもちろん間違いではなかろうが)。どうやら 本当の意味は、処理するテキストの先頭を示しているのではないかとも思える。 まあ、ここでは無難に「text[] のインデックス(でもある)」とだけ理解しよう。 既に図には書き込んである。 次の if だが、remainder が matchlen よりも小さい場合の条件だ。また、 matchlen の予想が覆されそうな予感がしないでもないが、この if 文は*例外 条件*なので無視することにした。都合の悪いことは見ない方が良いのだ。 /* (C) */ hval = ((((text[dicsiz] << 5) ^ text[dicsiz + 1]) << 5) ^ text[dicsiz + 2]) & (unsigned)(HSHSIZ - 1); (C) である。これは難解である。複雑な数式は苦手であるが、じっくり考えよ う。まず求める値は hval である。これは hash[] のインデックスなのだが、 このような複雑な式で求まるインデックスなんて想像もつかない。まず、最初 のインスピレーションを大事にすることにしよう。冒頭で、(C) の処理は「ハッ シュ値 hval を計算する。」っと苦もなく予想した。そしてこれは間違いでは ないだろう(きっと)。hash[] との関連をここで考えてもわからないから、こ のハッシュ値の計算だけを考えることにしよう。 式をじっくり見てみる。。。じっくり見てみると以下のことがわかる。 x(i) = text[dicsiz + i] とすると hval = ((x(0) << 5) ^ (x(1) << 5) ^ (x(2) << 0)) & (unsigned)(HSHSIZ - 1); である。最後の & (unsigned)(HSHSIZ - 1) は、前にも似たような式が出たが これはある範囲の数値(ここでは、0 〜 HSHSIZ{2^15}-1)を抽出するためのビッ トマスクである。ハッシュ関数と言うのはある符号をある集合の符号に写像す る関数であるからこのようなビットマスクは当然必要だし、良く行われる事だ (普通は mod 素数を行うんだけど)。また、hval は、hash[] のインデックス なのだから、写像する集合とは hash[] のインデックスだ。おっ、案外簡単に わかった。x(i) が text[dicsiz + i] で、ハッシュ関数の変数は x(0), x(1), x(2) だから、先頭の 3 バイトの文字列(平文)のハッシュ値を求めてい るわけだ。その他の計算(<< 5 とか ^ とか) は大したことではない。無視し よう。また、続けて (D) の処理も見るが、 /* (D) */ insert(); insert() は、幸い解読ずみである pos を hash[] に格納する処理だ。 予想の段階では、(C) と (D) を別個の処理と考えていたのだがこれは どうやらセットである。 (C) pos の位置の 3 文字のハッシュ値を計算し (D) hash[ハッシュ値] = pos を行う もう少し注意深く検討すると「posの位置の3文字」と、求めた「ハッシュ値」 は論理的には = である。 つまり、(C) (D) の処理は hash[文字列] = 位置 という処理を行っている。ハッシュ値の衝突はここでは考えない。slide 辞書 法では、ある文字列に対し以前その文字列が現れたかどうかを検索し、その位 置を求める必要があるのだが、この最初の 3 文字に関しては現時点でその用 件(位置を求める)を満たす事ができている。ここまでで自ずと encode() の全 体像も予想がつきそうな気がする。 # 衝突は考えないっとしたが prev[pos] に以前のハッシュ値が格納されてい # ることから簡単にわかる。チェーン法だ。謎は全て解けた。 # それにつけても、(C) と (D) の部分を見るだけでもこのソースがちょっと # 汚いことがわかる。もう少し、引数とか戻り値とか考えてくれても良かっ # たはずだ。ハッシュ関数にしても少なくともマクロぐらいにはしようよ。 (E) 〜 (H) に移ろうこれはループの中身で、処理の本題だ。まずループの脱 出条件を見てみると while (remainder > 0 && ! unpackable) { remainder は、バッファ上に読み込んだ平文の長さであるからこれがなくなる までループすることになる。さらに unpackable というのは、crcio.c の putcode() でその値を設定している箇所が出て来るのだが、符号化した出力サ イズが元のサイズを越えたときに真になる。つまり、これ以上処理しても圧縮 の意味がないとわかったらループを抜けるわけだ。 では、(E)を見よう。 /* (E) */ lastmatchlen = matchlen; lastmatchoffset = pos - matchpos - 1; --matchlen; ちょっと見ただけではやはりわからない。これらの変数はまだ予想しかしてな いからである。が、わかるだけの情報は書きしるそう。 ---------------------------------------------------------------------------- lastmatchlen 以前の matchlen の値 (変数名から) lastmatchoffset 以前マッチした位置 (変数名から) ---------------------------------------------------------------------------- 以前の値をlast〜に退避し、新たな値を設定する準備をしているわけだ。そし て、「新たな値の設定」は、--matchlen で早速行われている。しかし、「マッ チした長さ」をまだ何もしてないのに -1 するというのはいったいどういうこ とだろう? matchlen はループの頭で 2 に設定されている。これが 1 になっ た。本当の初期値は 1 なのか? ---------------------------------------------------------------------------- < 各変数の初期値 > matchlen = 1 matchpos = 0 pos = dicsiz lastmatchlen = 2 lastmatchoffset = dicsiz - 1 (pos - matchpos - 1) ---------------------------------------------------------------------------- この (E) はまた後で見る事になるだろう。 (F) (G) である。また、その直後には以前にも見た境界条件がある。 /* (F) */ /* (G) */ get_next(); match_insert(); if (matchlen > remainder) matchlen = remainder; if 文 は無視して関数の中身だけを追いかけてみよう。まず、get_next() こ れは 次の token を取ってくる処理だと予想してある。はたしてどうだろうか? static void get_next() { remainder--; if (++pos >= txtsiz - maxmatch) { update(); } hval = ((hval << 5) ^ text[pos + 2]) & (unsigned)(HSHSIZ - 1); } remainder を消費し、pos を進めている。予想通りだ。ひとまず if の条件は 無視すると直後で hash 値を求め直している。このハッシュ関数は、以前のハッ シュ値を利用しているが、これは pos が以前より + 1 されていることを考え ると関連が見えて来る。以前のhash関数を pos の関数として書き直すと x(pos+i) = text[pos + i] hval(pos) = (x(pos+0) << 5) ^ (x(pos+1) << 5) ^ (x(pos+2) << 0) & (unsigned)(HSHSIZ - 1); であり、また、今度のハッシュ関数は、 hval(pos+1) = (hval(pos) << 5) ^ (x(pos+1 + 2) << 0) & (unsigned)(HSHSIZ - 1); だ、繁雑なので & (HSHSIZE-1) を外すと、(気をきかして << 0 というのも書 いていたがどうやら意味がないので外した) hval(pos+1) = (x(pos+0) << 5) ^ (x(pos+1) << 5) ^ x(pos+2) ^ x(pos+3) っとなる。この次 get_next() が呼び出されれば、 hval(pos+2) = (x(pos+0) << 5) ^ (x(pos+1) << 5) ^ x(pos+2) ^ x(pos+3) ^ x(pos+4) である。順にハッシュ値を求める文字列長を増やしている。とにかく、 get_next() は、pos を進め、remainder を縮め、新たな(以前より1文字長い) 文字列のハッシュ値 hval を求める関数のようだ。 しかし、いつまでも hash 値の元となる文字列を伸ばしてもしょうがないだろ う。hval はどこかでまたリセットされるはずだ。っと思ってソースを探して みたがそのような箇所は見当たらない。なぜだろう?考えてみる・・・考えて みたがわからなかった。仕方ないので後にしよう。 ところで、前回、hval の計算とinsert() はセットだと言った。今回はどうだ ろう?次の match_insert() を見てみる。 /* 現在の文字列と最長一致する文字列を検索し、チェーンに追加する */ static void match_insert() { ... 省略 ... prev[pos & (dicsiz - 1)] = hash[hval]; hash[hval] = pos; } ・・・強敵であった。強敵すぎたので逃げて、最後の2 行だけに着目した。こ れは、insert()と同じだ。予想は当たっている。get_next() で hval を更新 した後は、この match_insert() で、prev[] と hash[] を更新するわけだ。 そして、match_insert() の省略した部分は、どうやら matchpos, matchlen, too_flag を更新しているだけのようだ。これが本当なら match_insert()で、 insert()の処理をせず、関数を分けるかした方が良さそうだ。(真偽の程は詳 細を見てからになる) おもむろに後続の処理 (H) を見ると、 /* (H) */ if (matchlen > lastmatchlen || lastmatchlen < THRESHOLD) { これが真なら「見つからなかった状態」と予想した(なぜだろ?)。そして、 lastmatchlen は初期状態では 2 である。予想した条件は逆か? matchlen ま わりは予想ばかりで進めすぎた。そしてどうやら match_insert() を読みとか なければこの先も分からずじまいになりそうだ。 このまま match_insert() を詳細に解析する事にしよう。 ...ひとまず休む。 From JCA02266 @ nifty.ne.jp Mon Jan 6 00:20:16 2003 From: JCA02266 @ nifty.ne.jp (Koji Arai) Date: Mon, 06 Jan 2003 00:20:16 +0900 (JST) Subject: [Lha-users] The Hacking of LHa for UNIX (1st draft) In-Reply-To: <20030105.130511.59492744.JCA02266@nifty.ne.jp> References: <20030105.130511.59492744.JCA02266@nifty.ne.jp> Message-ID: <20030106.002016.41659741.JCA02266@nifty.ne.jp> 新井です。 更新。この資料は、lha の CVS リポジトリに登録した。 Index: Hackinf_of_LHa =================================================================== RCS file: /cvsroot/lha/lha/Hackinf_of_LHa,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- Hackinf_of_LHa 5 Jan 2003 15:13:29 -0000 1.1 +++ Hackinf_of_LHa 5 Jan 2003 15:15:39 -0000 1.2 @@ -1,7 +1,8 @@ The Hacking of LHa for UNIX (1st draft) ------------------------------------------- - 2003-01-05 Koji Arai +$Id: Hackinf_of_LHa,v 1.2 2003/01/05 15:15:39 arai Exp $ + Koji Arai 本書は、LHa for UNIX 1.14i のソースを解読し、その圧縮アルゴリズムの実 装を確認するためのものだ。この成果は別の形でまとめなおされ資料になるか @@ -617,8 +618,27 @@ 件(位置を求める)を満たす事ができている。ここまでで自ずと encode() の全 体像も予想がつきそうな気がする。 -# 衝突は考えないっとしたが prev[pos] に以前のハッシュ値が格納されてい -# ることから簡単にわかる。チェーン法だ。謎は全て解けた。 +衝突は考えないっとしたが、ちょっと考えたらすぐわかった。prev[] には、 +以前のハッシュ値で求めた文字列の位置が入っている。つまり、prev[] はハッ +シュが衝突したときのためのバッファだ。このハッシュはチェーン方だ。 + +例えば、insert() で、 + prev[pos] = hash[hval]; + hash[hval] = pos; +っと処理をしているのだから + + hash[hval] = pos1 + | + v + prev[pos1] = pos2 + | + v + prev[pos2] = pos3 + ... + +といった値が入る事になる。ある文字列(のハッシュ値) hval に対して、その +辞書上の位置は pos1, pos2, pos3 という候補があるわけだ。実際にどの pos +を選ぶかは比較によって行われるのだろう。 # それにつけても、(C) と (D) の部分を見るだけでもこのソースがちょっと # 汚いことがわかる。もう少し、引数とか戻り値とか考えてくれても良かっ @@ -645,8 +665,8 @@ いからである。が、わかるだけの情報は書きしるそう。 ---------------------------------------------------------------------------- -lastmatchlen 以前の matchlen の値 (変数名から) -lastmatchoffset 以前マッチした位置 (変数名から) +* lastmatchlen 以前の matchlen の値 (変数名から) +* lastmatchoffset 以前マッチした位置 (変数名から) ---------------------------------------------------------------------------- 以前の値をlast〜に退避し、新たな値を設定する準備をしているわけだ。そし @@ -729,8 +749,6 @@ ところで、前回、hval の計算とinsert() はセットだと言った。今回はどうだ ろう?次の match_insert() を見てみる。 -/* 現在の文字列と最長一致する文字列を検索し、チェーンに追加する */ - static void match_insert() { ... 省略 ... @@ -757,4 +775,523 @@ わりは予想ばかりで進めすぎた。そしてどうやら match_insert() を読みとか なければこの先も分からずじまいになりそうだ。 -このまま match_insert() を詳細に解析する事にしよう。 +このまま match_insert() を詳細に解析する事にしよう。match_insert() +をすべて再掲する。 + +/* 現在の文字列と最長一致する文字列を検索し、チェーンに追加する */ + +static void match_insert() +{ + unsigned int scan_pos, scan_end, len; + unsigned char *a, *b; + unsigned int chain, off, h, max; + + max = maxmatch; /* MAXMATCH; */ + if (matchlen < THRESHOLD - 1) matchlen = THRESHOLD - 1; + matchpos = pos; + + off = 0; + for (h = hval; too_flag[h] && off < maxmatch - THRESHOLD; ) { + h = ((h << 5) ^ text[pos + (++off) + 2]) & (unsigned)(HSHSIZ - 1); + } + if (off == maxmatch - THRESHOLD) off = 0; + for (;;) { + chain = 0; + scan_pos = hash[h]; + scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; + while (scan_pos > scan_end) { + chain++; + + if (text[scan_pos + matchlen - off] == text[pos + matchlen]) { + { + a = text + scan_pos - off; b = text + pos; + for (len = 0; len < max && *a++ == *b++; len++); + } + + if (len > matchlen) { + matchpos = scan_pos - off; + if ((matchlen = len) == max) { + break; + } + } + } + scan_pos = prev[scan_pos & (dicsiz - 1)]; + } + + if (chain >= LIMIT) + too_flag[h] = 1; + + if (matchlen > off + 2 || off == 0) + break; + max = off + 2; + off = 0; + h = hval; + } + prev[pos & (dicsiz - 1)] = hash[hval]; + hash[hval] = pos; +} + +まず、初期化部分の前半 + + max = maxmatch; /* MAXMATCH; */ + if (matchlen < THRESHOLD - 1) matchlen = THRESHOLD - 1; + matchpos = pos; + + off = 0; + +maxmatch は、固定値で 256 だ、だから max も 256 +2行目の if 文は、これまでしつこいくらいに出て来た条件に似ているが、今 +回は条件を満たすらしい。これまでは、 + + if (matchlen > remainder) matchlen = remainder; + +という条件だった。そして今回は、 + + if (matchlen < THRESHOLD - 1) matchlen = THRESHOLD - 1; + +だから、全体的に matchlen の値は、 + + THRESHOLD-1 <= matchlen <= remainder + +つまり、 + + 2 <= matchlen <= バッファに残ったテキスト長 + +の範囲に納められるようだ。ここでは、matchlen は下限値を下回るので2 に +設定される。次に matchpos, off が初期化され。以下の図の状態になる。 +(pos, remainder は、get_next() で更新されていることに注意) + +---------------------------------------------------------------------------- + + dicsiz=2^dicbit 2*2^dicbit + v v txtsiz + +-------------+-------------+-------------+-------------+---+ + text | | | | | | + +-------------+-------------+-------------+-------------+---+ + `-pos(=dicsiz+1) <---> + matchpos(=pos) maxmatch{256} + off(=0) + + <------ remainder ------------> + + |--- この位置に最初の ---------| + データが読まれている +---------------------------------------------------------------------------- + +初期化部分の後半 + + for (h = hval; too_flag[h] && off < maxmatch - THRESHOLD; ) { + h = ((h << 5) ^ text[pos + (++off) + 2]) & (unsigned)(HSHSIZ - 1); + } + if (off == maxmatch - THRESHOLD) off = 0; + +h は、too_flag[] が今のところすべて0だから hval だ。(too_flag は、h つ +まり hval をインデックスに取るらしい。hash[] と同じだ。再掲はしないが +メモに書き加えておこう) + +off は、pos の位置からのオフセットのようだ(h を更新する for 文の中身か +ら)。図もその位置に書いた。最後の if 文は off が上限に達した場合に0 に +再初期化している。よくわからないので無視しよう。for 文の中身からh や +off の用途はどうも先読みしたハッシュ値とその先読みの位置なのではないか +と想像する。too_flag[] の状態によって先読みすべき値が変わるのだろうか? + +とにかく処理の本題に入る事にしよう。まず、この関数に現れる局所変数を列 +挙しておこう + + unsigned int scan_pos, scan_end, len; + unsigned char *a, *b; + unsigned int chain, off, h, max; + +とりあえず、off, h, max はすでに出て来たので残りは + + scan_pos, scan_end, len, a, b, chain + +だ、これだけの変数の意味を解読しなくてはならない。変数は状態を表すから、 +その数が多いと言うのはそれだけ複雑な処理だということだ。めげる。 + +この関数のメインとなるループの中をざっと眺めてみるさらに内部にループが +ある。ひとまず、二重ループの中身を省略しよう。 + + for (;;) { + chain = 0; + scan_pos = hash[h]; + scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; + + while (scan_pos > scan_end) { + chain++; + ... 略 ... + } + + if (chain >= LIMIT) + too_flag[h] = 1; + + if (matchlen > off + 2 || off == 0) + break; + max = off + 2; + off = 0; + h = hval; + } + +まず、前半部分から + + chain = 0; + scan_pos = hash[h]; + scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; + +chain, scan_pos, scan_end はすべて while ループに渡されるべき変数だ。 +さらに、while の後には、scan_pos, scan_end は現れないから(仮に while +ループが1つの関数だったとすると)これらは while ループ部の引数(入力)だ。 +この2つの変数はどうやりくりしようとも、while ループ部内の状態しか表さ +ないので、ここでは無視しよう。 + +while ループの後を見てみると + + if (chain >= LIMIT) + too_flag[h] = 1; + + if (matchlen > off + 2 || off == 0) + break; + max = off + 2; + off = 0; + h = hval; + +chain が LIMITを越えた場合、too_flag[h] = 1 としている。chain は、ざっ +と見て、while ループのカウンタらしいが、LIMIT は 0x100 だ。どうにも例 +外条件っぽい(LIMITという名前や数値がそう思わせる)のでここでは無視しよ +う。while ループが 256以上回る可能性がある点だけ心にとどめておこう。 + +次の条件では、matchlen と off が条件判断されている。ということはこのど +ちらか、あるいは両方は while ループの返り値(出力)だ。ざっと +match_insert()全体を見てみると off は最初とこの直後でしか更新されない +らしい。つまり、while ループ部の返り値はmatchlen の方だ。 +この条件は for () ループの脱出条件でもある。心にとどめて、次に進む。 + + max = off + 2; + off = 0; + h = hval; + +ふむ。よくわからない。しかし注目すべき点はある。off はここで、0 になる +がこれ以降は off の値は変わらない。つまり、off は最初は何らかの値で +while ループ部に渡されるが、その次からは、0 だ。この for ループが何度 +回ろうとも 0 だ。h も同じで最初は何らかの値を持つが、2回目のループ以降、 +h は hval だ。max は、off を 0 にする直前に更新しているから、h や off +と事なり、3つの状態を持つ、すなわち。maxmatch, off+2, 2 だ。 + +いや、脱出条件を見てみると off == 0 なら break とある。つまり、この +for ループはどんなに頑張っても2回しか回らないらしい。やっぱり max も 2 +つの状態しか持たないようだ。 + +これで、1 回目、2回目に while ループ部に入る直前の状態が書ける。この関 +数 match_insert() は、while ループ部を1回か2回実行する処理と言うわけだ。 + +ここで無視していた。while ループ部の入力となる scan_pos, scan_end +もそれぞれどのような状態になるか書いておく + +---------------------------------------------------------------------------- +< 1回目 > + h = 何か + off = 何か + max = maxmatch + + scan_pos = hash[h] + scan_end = pos + off - dicsiz (あるいは、off) + + matchlen = 2 + matchpos = pos +< 2回目 > + h = hval + off = 0 + max = 前の off + 2 + + scan_pos = hash[hval] + scan_end = pos - dicsiz (あるいは、0) + + matchlen = ? + matchpos = ? +---------------------------------------------------------------------------- + +上記は一般化した場合だが、今回(初回)の場合、h や off の値は、hval であ +り、0 だった。2回目ループのときの状態と同じである。2回のループの違いは +max の値がmatchpos であるか off+2 (すなわち2)であるかの違いしかないようだ。 + +ここは、条件を少なくするためにこの場合だけにしぼって処理を考えよう。 +while ループの2回の呼び出しを行う際の状態は以下の通りに書き直せる。 + +---------------------------------------------------------------------------- +< 1回目 > + h = hval + off = 0 + max = maxmatch + + scan_pos = hash[hval] + scan_end = pos - dicsiz (あるいは、0) + + matchlen = 2 + matchpos = pos +< 2回目 > + h = hval + off = 0 + max = 2 + + scan_pos = hash[hval] + scan_end = pos - dicsiz (あるいは、0) + + matchlen = ? + matchpos = ? +---------------------------------------------------------------------------- + +うーん、まだ、すっきりしない。何がすっきりしないかというと scan_end の +値だ。これが何を意味するのかがよくわからない。scan_pos は、わかるのか? +というと、わかる。hash[hval]だから現在の文字列と同じ文字列の辞書上の位 +置だ。さらに、現時点では get_next() で、hval を更新してから insert() +を行っていないので、hash[hval] には何も入っていない。すなわち 0 だ。 + + scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; + +を考えよう。off は、0 だから + + scan_end = (pos > dicsiz) ? pos - dicsiz : 0; + +なわけだ。さらに、posは現時点で dicbit+1 であるから、1 だ。図に書こう。 + +---------------------------------------------------------------------------- + + dicsiz=2^dicbit 2*2^dicbit + v v txtsiz + +-------------+-------------+-------------+-------------+---+ + text | | | | | | + +-------------+-------------+-------------+-------------+---+ + ^ ^ `-pos(=dicsiz+1) + | | + | scan_end はこの辺(1) + scan_pos はこの辺(0) + + h = hval + off = 0 + max = 2 + +---------------------------------------------------------------------------- + +ついに、text[] バッファの左半分に指しかかる。これが何なのかは現時点で +は明確に書いてなかったが予想するとこの左半分はズバリ辞書だ。言い切って +やろう。今まで辞書らしい(dicsizのサイズを持つ)バッファは hash[] や +prev[] があったが、hash[], prev[] の用途はもう明確である。辞書となり得 +るバッファはもうこの text[] しかないのだ。 + +さらに、左半分に限らずこの text[] 全体が辞書であろうと予想する。これは +ただの勘だが text[] は環状バッファなのではなかろうかと考えている。 + +# 最初の方で prev[] が辞書だと予想したが間違った予想をしていたことにこ +# の時点で気づいた。prev[] が辞書と同じサイズを持つ理由はまだよくわか +# らない。 + +この時点ではまだ scan_pos や scan_end の真の意味はわからない。off のこ +とを無視しているから予想も立ちにくいが、ひとまず初期状態がどういったも +のかはわかったのでこのまま、while ループ部を見てみたいと思う。 + + while (scan_pos > scan_end) { + chain++; + + if (text[scan_pos + matchlen - off] == text[pos + matchlen]) { + { + a = text + scan_pos - off; b = text + pos; + for (len = 0; len < max && *a++ == *b++; len++); + } + + if (len > matchlen) { + matchpos = scan_pos - off; + if ((matchlen = len) == max) { + break; + } + } + } + scan_pos = prev[scan_pos & (dicsiz - 1)]; + } + +まず、if 文の条件を満たさない場合だけを考える。 + + while (scan_pos > scan_end) { + chain++; + + if (text[scan_pos + matchlen - off] == text[pos + matchlen]) { + ... + } + scan_pos = prev[scan_pos & (dicsiz - 1)]; + } + + +offは 0 なので、text[scan_pos + matchlen] != text[pos + matchlen] という条件の場合を想定するわけだが、 + +text[scan_pos + matchlen] + +と + +text[pos + matchlen] + +を比べている + +text[scan_pos] 辞書上の文字列の*先頭* +text[pos] 現在の文字列の*先頭* + +を比べないのは matchlen が前に予想した「最低限一致しなければならない長さ-1」 +だからであろう。現時点で、matchlen が 2 だから + +text[scan_pos + 0] == text[pos + 0] +text[scan_pos + 1] == text[pos + 1] + +であったとしても、 + +text[scan_pos + 2] != text[pos + 2] + +であれば、「最低限一致しなければならない長さ」という条件を満たさないの +である。なので matchlen の位置から先に比較して無駄な比較をしないように +している。後でちゃんとした比較の処理が出て来るだろう。このような処理は +処理としては効率が良いのだが、ことソース理解と言う点では冗長である。わ +かりにくいのだ。仕方ないのだけど。 + +# matchlen の意味の予想はどうやら当たっているようだ。matchlen は最短一 +# 致長で、minmatchlen っと名付けても良さそうな変数だ。 + +そして、比較に失敗したら scan_pos を更新する。 + + scan_pos = prev[scan_pos & (dicsiz - 1)]; + +ハッシュのチェーンをたどっている、つまり次の候補を辞書から取り出してい +るわけだ。ここまでで、while ループの処理内容の想像はついた。このループ +は辞書から(最長)一致する文字列を探しているのだろう。 + +順番が前後したが、while ループの脱出条件を見てみる + + while (scan_pos > scan_end) { + +これはどういうことだろう? scan_pos は、ハッシュのチェーンをたどって同 +じハッシュ値を持つ文字列の位置を探す、この値はだんだんと小さくなって行 +くものなのだろうか? +その通りである。hash[] への格納はファイルから取って来た文字列を順に格 +納して行くのでチェーンの奥には、より前の方の位置が書かれているはずだ。 +逆にチェーンの浅い部分にはより現在位置に近い、位置が書かれているのだろ +う。では、その境界 scan_end はどうやってわかるのだろうか?これは後でま +た検証しよう。 + +では、処理の本質 if 文の中を見る事にしよう + + if (text[scan_pos + matchlen - off] == text[pos + matchlen]) { + { + a = text + scan_pos - off; b = text + pos; + for (len = 0; len < max && *a++ == *b++; len++); + } + + if (len > matchlen) { + matchpos = scan_pos - off; + if ((matchlen = len) == max) { + break; + } + } + } + +最初の意味もなくブロックになっている部分を見る、 + + { + a = text + scan_pos - off; b = text + pos; + for (len = 0; len < max && *a++ == *b++; len++); + } + +この処理では a, b といういかにも局所な名前の変数が使われている。これは、 +本当にこのブロック内にで局所的なもののようだ。ならば定義位置もこのブロッ +ク内にして本当に局所的にして欲しかった。 + +さらに、この処理は単に文字列 a, b を比較しているだけのようだ。memcmp() +ではまずいのかと言うとここで求めているものが「どこまで一致したか(len)」 +のようなので、memcmp() では役不足だ。仕方ないので関数をでっちあげて抽 +象化をはかろう。memcmp_ret_len()(我ながら変な名前だ)という関数があった +とするとこの部分は + len = memcmp_ret_len(&text[scan_pos-off], &text[pos], max); + +っとなる。返り値は一致した文字列長だ。 + +その次の処理、 + + if (len > matchlen) { + matchpos = scan_pos - off; + if ((matchlen = len) == max) { + break; + } + } + +で、matchlen (最低一致長)より大きい場合に条件を満たす。条件を満たさな +ければ、scan_pos を更新する分けだが、この if の外側の if と同じ条件判 +断を行っている。外側の if は単に効率のための冗長な処理だったのは前にも +書いたとおりだ。今更だが、外側の if はソース解析と言う点では無視しても +良いものだった。さて、とにかくこの if 文を見てみよう。まず最短一致長の +一致条件を満たした場合、matchpos と matchlen を更新する。 + +matchpos はマッチした位置、 +matchlen はマッチした長さ + +で、matchlen が max なら最長一致長に達しているので、これ以上探索はしな +い。matchlen は最短一致長でありながら、一致長でもある変数のようだ。 +(どうやら以前の2つの予想はどちらも当たっていた模様) + +とにかく while ループ部の出力は、この matchpos と matchlen のようだ。 +前に書いた通りこのループは「最長一致文字列を求める処理」だ。 + +match_insert() の全体をもう一度見てみよう。ただし、以下の書き換えを行う。 + +o while ループ部は search_dict(pos, scan_pos, scan_end, max) という関数 + に置き換えたものとする。 + +o 末尾の insert() と同等の処理を行っている部分も insert() の呼び出しに + すり替えよう。(match_insert() 関数の中で insert() 処理を本当に行うべ + きものなのかどうかが疑問だが) + +o chain という変数にかかわる処理も隠した(search_dict内で行う) + +static void match_insert() +{ + unsigned int off, h, max; + + max = maxmatch; /* MAXMATCH; */ + if (matchlen < THRESHOLD - 1) matchlen = THRESHOLD - 1; + matchpos = pos; + + off = 0; + for (h = hval; too_flag[h] && off < maxmatch - THRESHOLD; ) { + h = ((h << 5) ^ text[pos + (++off) + 2]) & (unsigned)(HSHSIZ - 1); + } + if (off == maxmatch - THRESHOLD) off = 0; + for (;;) { + unsigned int scan_end; + + scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; + + search_dict(pos, hash[h], scan_end, max); + + if (matchlen > off + 2 || off == 0) + break; + max = off + 2; + off = 0; + h = hval; + } + + insert(); +} + +だいぶすっきりした。まだ、off にかかわる部分がよく分からないが、ひとま +ず良いだろう。この関数の解析はこれで終って良いだろうか。 + +いや、良くない。肝心の match_insert() の出力がよくわからない。この関数 +は「最長一致文字列を探し、hash を更新する処理」(くどいようだが、hashを +更新するは余計に思う)なのだろうが、最長一致文字列が見つからなかったと +いうのはどう判断されるのだろう? + +まず、search_dict() で見つからなかった場合、matchlen, matchpos は更新 +されない。そして、おそらく 2 度目の for ループが行われるが、too_flag[] +というので、判断できそうな気もするがこれはむしろハッシュのチェーンをた +どりすぎるのを止めるためのフラグであるように思える。 + +2度目のループで、max の値が変わるのが鍵だろうか?。今回の場合、max は +256 から 2 になる。最長一致長として 2 が限界値になると、search_dict() +の動作は変わるだろうか?いや、やはり変わらない。どうにもこの関数だけで +は見つかったか見つからなかったかという判断はできないようだ。 + +気持悪いがやはりこの関数の解析を終え、次に移る事にしよう。 -- 新井康司 (Koji Arai) From tsugio @ muc.biglobe.ne.jp Mon Jan 6 21:01:33 2003 From: tsugio @ muc.biglobe.ne.jp (Tsugio Okamoto) Date: Mon, 06 Jan 2003 21:01:33 +0900 Subject: [Lha-users] The Hacking of LHa for UNIX (1st draft) In-Reply-To: <20030106.002016.41659741.JCA02266@nifty.ne.jp> References: <20030105.130511.59492744.JCA02266@nifty.ne.jp> <20030106.002016.41659741.JCA02266@nifty.ne.jp> Message-ID: <20030106205247.54BE.TSUGIO@muc.biglobe.ne.jp> 岡本です. なんか申し訳ないです.少しコメントします. あと,サンプルコードを送付します. offは検索を早くするための名残です.lhaを誕生させる試行錯誤した結果です. もとは,こんなに汚いソースではなかったのです. > 何も予備知識がない状態で書けば > > while (read_file(&token, tokensiz)) { > len = search_dict(dict, token, &pt); > if (len == -1) { > print_token(token); > else > print_pair(len, pt); > update_dict(dict, token); > } > > のようになるはず。ここで、tokensiz は token の最大サイズで、最長一致長 > を表す。この値が大きければ大きい程、圧縮効率は良くなるはずで、lha では、 > これは MAXMATCH{256}である。また、dict は辞書でこのサイズは lha の > -lz5- メソッドでは、8192 となっている。この辞書も大きければ大きい程良 > いはずだ。その方が一致文字列が見つかりやすい。(ただし、辞書が大きいと > 一致位置を示す情報 の情報量が増えるはずだし、速度も遅くなる > だろう。後で検証する) > > で、実際にソースを見てみると(slide.c:encode())・・・、まったくこのよう > な構造にはなってないように見える。何やらややこしいことばかりでまったく > わからない。なぜここまでややこしいのかと泣きたくなってくるが、それは速 > 度のためである(本当?)。上記のコードで、search_dict() は、単に dict か > ら token に一致する位置を検索するだけで良さそう(実際にそれでも良い)だ > が、これではまったく速度が出ない。このあたりの工夫が slide 辞書法のキ > モである。 はい. text[i+0] text[i+1] text[i+2] をハッシュ関数で変換して,ハッシュテーブルに登録します. で,高速に検索できるようにするというのが工夫です. lha v1.00を見るとまた違う構造になっていると思います. > そういうわけで、この部分を読み解くことにする。なお、予備知識として lha > では、辞書から token を探すのにハッシュが使われているらしいことを記し > ておく。 > > ここでは実際にデバッガで動作させながら解読するのではなく、ソースを読む > だけで理解できるかを試すことにする。また、本文は某書(謎)のノリをマネて > いると指摘する方がいるかもしれない・・・がまったくその通りだ。 > > まず、そのものずばりの encode() (slide.c) を見る。以下がこの関数だが > 処理の要点だけに着目するために不要そうな部分は(現時点で予測で)削った。 > > unsigned int > encode() > { > int lastmatchlen; > unsigned int lastmatchoffset; > > /* (A) */ > init_slide(); > > /* (B) */ > remainder = fread_crc(&text[dicsiz], txtsiz-dicsiz, infile); > encoded_origsize = remainder; > matchlen = THRESHOLD - 1; > > pos = dicsiz; > > if (matchlen > remainder) matchlen = remainder; > > /* (C) */ > hval = ((((text[dicsiz] << 5) ^ text[dicsiz + 1]) << 5) > ^ text[dicsiz + 2]) & (unsigned)(HSHSIZ - 1); > > /* (D) */ > insert(); > > while (remainder > 0 && ! unpackable) { > /* (E) */ > lastmatchlen = matchlen; lastmatchoffset = pos - matchpos - 1; > --matchlen; > > /* (F) */ /* (G) */ > get_next(); match_insert(); > if (matchlen > remainder) matchlen = remainder; > > /* (H) */ > if (matchlen > lastmatchlen || lastmatchlen < THRESHOLD) { > /* (H.1) */ > encode_set.output(text[pos - 1], 0); > count++; > } else { > /* (H.2) */ > encode_set.output(lastmatchlen + (UCHAR_MAX + 1 - THRESHOLD), > (lastmatchoffset) & (dicsiz-1) ); > --lastmatchlen; > > while (--lastmatchlen > 0) { > get_next(); insert(); > count++; > } > get_next(); > matchlen = THRESHOLD - 1; > match_insert(); > if (matchlen > remainder) matchlen = remainder; > } > } > } > > まず、この関数から概観を見てみると、ループの前に初期化処理として > 以下が行われている。 > > (A) init_slide() 初期化する > (B) ファイルを読み込み text[] に格納する。 > (C) ハッシュ値 hval を計算する。 > (D) insert() する (きっと辞書に token を追加しているのだろう) > > そして、ループ処理では以下の処理が行われている > > (E) lastmatchlen, lastmatchoffset, matchlen を更新する。 > (F) get_next() (次の token を読む。たぶん) > (G) match_insert() (辞書に追加する。たぶん) > > (H) matchlen > lastmatchlen || lastmatchlen < THRESHOLD なら > > (H.1) output() する。(マッチしなかったらそのまま出力しているのだろう。たぶん) > (H.2) そうでなければ(マッチしたなら)、output()し、何かいろいろする。 > > 現時点で、(H.2) の部分はよく解読できなかった。何やら再度 get_next() が > 呼ばれていたりして思った通りの処理フローにはなっていない。だが、ここで > は焦らず放置することにして、ここまで予想で書いた部分の細部に触れること > にする(単にここまでの予想が間違っているだけかもしれないのだから、わか > らない部分を無理にわかるように頑張る必要はなかろう) > > 関数の細部に触れる前にデータ構造について調べておく。データ構造に対して > の理解が深まればアルゴリズムの80%は分かったも同然だ(誇張)。slide.c で > 使用されているデータ構造は以下の通りだ。(不要そうだと思うものは除いて > ある) > > static unsigned int *hash; > static unsigned int *prev; > unsigned char *too_flag; > static unsigned int txtsiz; > static unsigned long dicsiz; > static unsigned int hval; > static int matchlen; > static unsigned int matchpos; > static unsigned int pos; > static unsigned int remainder; > > too_flag だけ、static がついてないが、他のソースを grep してもこの変数 > を使っている箇所はない、単に static の付け忘れだろう。 多分. > これらの変数は、encode() の冒頭 init_slide() で初期化されている・・の > かと思ったら違った。slide.c:encode_alloc() で行われている。 > > int > encode_alloc(method) > int method; > { > if (method == LZHUFF1_METHOD_NUM) { /* Changed N.Watazaki */ > encode_set = encode_define[0]; > maxmatch = 60; > dicbit = 12; /* 12 Changed N.Watazaki */ > } else { /* method LH4(12),LH5(13),LH6(15) */ > encode_set = encode_define[1]; > maxmatch = MAXMATCH; > if (method == LZHUFF7_METHOD_NUM) > dicbit = MAX_DICBIT; /* 16 bits */ > else if (method == LZHUFF6_METHOD_NUM) > dicbit = MAX_DICBIT-1; /* 15 bits */ > else /* LH5 LH4 is not used */ > dicbit = MAX_DICBIT - 3; /* 13 bits */ > } > > dicsiz = (((unsigned long)1) << dicbit); > txtsiz = dicsiz*2+maxmatch; > > if (hash) return method; > > if (alloc_buf() == NULL) exit(207); /* I don't know this 207. */ > > hash = (unsigned int*)malloc(HSHSIZ * sizeof(unsigned int)); > prev = (unsigned int*)malloc(DICSIZ * sizeof(unsigned int)); > text = (unsigned char*)malloc(TXTSIZ); > too_flag = (unsigned char*)malloc(HSHSIZ); > > if (hash == NULL || prev == NULL || text == NULL || too_flag == NULL) > exit(207); > > return method; > } > > 引数に渡された method (これは、lh1, lh5, lh6, lh7 などを示す)によって、 > 初期化される内容が変わる(encode_alloc()前半部分)。このことから各変数の > 用途もわかる。 > > method maxmatch dicbit > ---------------------------- > -lh1- 60 12 > -lh5- 256 13 > -lh6- 256 15 > -lh7- 256 16 > > ということらしい。dicbit というのは辞書サイズのbitサイズで、辞書サイズ > は 2^dicbit で表されている。lh5 が 8KB(2^13)、lh6 が 32KB(2^15)、lh7 > が 64KB(2^16) の辞書サイズを利用すると言うのは予備知識である。maxmatch > というのは、token の最長一致長である。このことも予備知識として詳細には > 触れない。(ところで、本書では当面、lh5, 6, 7 のことしか言及しない) > > encode_set, encode_define というのがあるが、method によって、Huffman > coding の方法を変えていることはちょっと見ればすぐにわかるし、大したこ > とではない。以降無視する。 > > encode_alloc() の後半では、他の変数の初期化(バッファの割り当て)が行われる。 > > dicsiz = (((unsigned long)1) << dicbit); > > dicsiz はそのものずばり辞書サイズである。 > > txtsiz = dicsiz*2+maxmatch; > > 現時点で txtsiz が何なのかはわからない。 > if (hash) return method; > > hash はこの直後で割り当てられる。つまり、一度割当を行ったら、 > encode_alloc() は、以降メモリの割当を行わない。ただそれだけだ。 > > if (alloc_buf() == NULL) exit(207); /* I don't know this 207. */ > > alloc_buf() は、huf.c で定義された関数。このことから Huffman coding の > ためのバッファを割り当てているのだろう。ここでは無視。(しかし、207 と > いうのは何なのだろう?) > > hash = (unsigned int*)malloc(HSHSIZ * sizeof(unsigned int)); > prev = (unsigned int*)malloc(DICSIZ * sizeof(unsigned int)); > text = (unsigned char*)malloc(TXTSIZ); > too_flag = (unsigned char*)malloc(HSHSIZ); > > if (hash == NULL || prev == NULL || text == NULL || too_flag == NULL) > exit(207); > > hash は、ハッシュ用の何か、HSHSIZ は、固定値で 2^15 である。 > > prev は、DICSIZから辞書だろう。要素の型が char でなく int であることに > も注目しておく。DICSIZ は dicsiz でも構わないはず。単に「大は小を兼ね > る」を実践しているだけであろう、TXTSIZ も同様である。おそらく、一度の > 実行で複数の圧縮メソッドを使用した場合、そのメソッド毎に領域を割り当て > るよりは最大の値をあらかじめ一度だけ割り当てた方が良いと考えたのだろう。 > しかし、ソースを参照するときは繁雑になるので以降、 > DICSIZ == dicsiz > TXTSIZ == txtsiz > であるとする。これ重要。 > > text は、現時点では不明 > > too_flag も不明 > > っとなる。まだ、良く分からないが、以下の図を書いておこう。後で何度も見 > ることになるだろう。この図はスケールが lh7 の場合を想定しているが。こ > のことは大したことではないはずだ。また、too_flag と hash のスケールが > 一緒だがこれはサイズ(領域のバイト数)が一緒なのではなく、要素数が一緒で > あることを示している。ほとんどの場合要素の型の違いというのは処理内容に > とって重要なことではないはずだ。 > > ---------------------------------------------------------------------------- > > 0 2^15=32768 > +-------------+ > hash | | > +-------------+ dicsiz=2^dicbit > +-------------+-------------+ 2*2^dicbit > prev | | | | > +-------------+-------------+ v txtsiz > +-------------+-------------+-------------+-------------+---+ > text | | | | | | > +-------------+-------------+-------------+-------------+---+ > <---> > maxmatch{256} > too_flag 2^15 > +-------------+ > | | > +-------------+ > ---------------------------------------------------------------------------- > > > 先に示した変数の中でまだ初期化には現れていないものがある。列挙すると > > static unsigned int hval; > static int matchlen; > static unsigned int matchpos; > static unsigned int pos; > static unsigned int remainder; > > だ、ざっとソースを眺めると slide.c:insert() という関数に > hash[hval] = pos; > というのが現れているから、hval は、hash[] の位置を指し、hash には、pos > を格納すると推測される。同様に > prev[pos & (dicsiz - 1)] = hash[hval]; > というのも現れているから pos は、prev[] の位置を指し、prev には、 > hash[hval] つまり、pos を格納しているようだ。これは少し謎な処理だが、 > insert() の全貌は短い(というかこれだけ)なので、ちょっと横道にそれて詳 > 細に見てみよう。(現在の解析の趣旨は、変数の用途の概要を予想すること) > > /* 現在の文字列をチェーンに追加する */ > > static void insert() > { > prev[pos & (dicsiz - 1)] = hash[hval]; > hash[hval] = pos; > } > > コメントはまったく意味不明だが、無視して処理内容に着目する。prev[] の > インデックス pos & (dicsiz - 1) は、dicsiz が 2^n であることからdicsiz > はビットマスクであることがわかる。例えば仮に dicsiz が 2^8 だと > dicsiz - 1 は、 > > 8 7 6 5 4 3 2 1 0 bit > -------------------------- > dicsiz 1 0 0 0 0 0 0 0 0 > dicsiz-1 1 1 1 1 1 1 1 1 > > である。このすべて 1 が立ったビットマスクと pos を & すると、どのよう > な pos の値に対しても pos & (dicsiz - 1) は、prev[] のインデックスの範 > 囲に納まる。もう少し言うと pos が仮にインデックスの最大値+1だった場合、 > pos & (dicsiz - 1) は、0 になる。これにより次の予想が立つ。 > > o pos が、prev[] の位置を指すのではなく、pos & (dicsiz - 1) が > prev[]の位置を指す。(pos は、このインデックスの範囲を越える可能性がある) > o それに反して、prev[] は環状バッファらしいという予想が立てばやはり > pos は、prev のインデックスである。 > > prev が環状バッファであると予想が付けば話が早い。pos & (dicsiz-1) は、 > pos と同義だと解釈可能だからである(prev が環状バッファでなく無限長のバッ > ファであると想像しよう)そして、pos & (dicsiz-1) を pos に置き換えて、 > 再度処理内容に着目すると > > prev[pos] = hash[hval]; > hash[hval] = pos; > > ということから、 > 1. (この関数に来る前に) pos が更新される。(予想) > 2. prev[pos] に以前の hash[hval] (以前の pos)を格納する > 3. hash[hval] に新しい pos を書く。 > といった処理であることが予想される。コメントの「チェーン」もなんとなく > 納得できる。新たな事実(まだ予想だが)が分かったので、図に書き記そう。 > > ---------------------------------------------------------------------------- > 0 2^15=32768 > +-+---+-------+ > hash | |pos|... | > +-+---+-------+ > `-hval > > .-----------. > v | > +----+-----+-------------------- > prev | |pppos| |ppos| . . . > +----+-----+-------------------- > `- ppos `-pos > > * hash の取り得る値は pos その位置は hval > * ppos は以前の pos を示す。pppos はさらに以前の pos を指す。 > * prev は無限長のバッファ(本当は環状バッファ) > ---------------------------------------------------------------------------- > > まだ、解析できてない変数が残っている。 > > static int matchlen; > static unsigned int matchpos; > static unsigned int remainder; > > しかしこれらはどうにもパッと見ではわからない。処理内容を追いかけないと > だめそうだ。仕方ないので変数名で予想しよう。(幸い前の変数名と違って予 > 想しやすい)以下 > > ---------------------------------------------------------------------------- > * matchlen 一致した文字列長 > * matchpos 一致した辞書上の位置 > * remainder token の残りサイズ > ---------------------------------------------------------------------------- > > はたして、予想はあっているのか、今はまだ分からない。 > > slide.c を見る限りデータ構造は網羅できた。結局分かったのか分からないの > か良く分からないが少しずつでも前進はしているはずだ。ここで、再度 > encode() の処理を追いかけよう。今度は細部にも着目する。 > > 前に、encode() のソースには (A) 〜 (H) までの記号を記した。この順番に > 解析を進めよう。 > > /* (A) */ > init_slide(); > > まあ、初期化である。内容を見てみると > > for (i = 0; i < HSHSIZ; i++) { > hash[i] = NIL; > too_flag[i] = 0; > } > > だけである。NIL というのは、0 であると slide.c で定義されている。普通 > このような初期値は、通常の値が取り得ない値を示している。NIL が 0 なら > hash[] に格納される pos は 0 にならない可能性がある。まあ、予想ばかり > 書いても仕方ないので、この関数は終ろう。余談だが、nil は null と同じで。 > 「ない」の意味だが、NULL がC言語ではポインタだから。別のマクロ名にした > のかも知れない。いずれにしてもこの程度はマクロにする必要もなかろうとは > 思うのは、余計なお世話かもしれない。 > > /* (B) */ > remainder = fread_crc(&text[dicsiz], txtsiz-dicsiz, infile); > encoded_origsize = remainder; > matchlen = THRESHOLD - 1; > > pos = dicsiz; > > if (matchlen > remainder) matchlen = remainder; > > ファイルを読み込み、各変数の初期値を設定している。注目すべきはファイル > を読み込んだバッファの位置である。fread_crc() は、crcio.c で定義された > 汎用関数で、CRC値を計算したり漢字コード変換をしたりを除けば、fread() > と同じである。つまり、ファイルは最初、 > > &text[dicsiz] の位置に、txtsiz-dicsiz 分だけ読まれる。 > > ことを示す。図示しよう。 > > ---------------------------------------------------------------------------- > < 初期状態 > > > dicsiz=2^dicbit 2*2^dicbit > v v txtsiz > +-------------+-------------+-------------+-------------+---+ > text | | | | | | > +-------------+-------------+-------------+-------------+---+ > `-pos <---> > maxmatch{256} > > <------ remainder --------------> > > |--- この位置に最初の ---------| > データが読まれている > ---------------------------------------------------------------------------- > > ますます、text[] の用途が不明だが、slide 辞書法の典型的な読み込み処理 > のことを考えるとある程度予想がつく(それを先に示した方が良いか?)。まあ、 > ここではフーンっと鼻で納得して済まそう。 > > fread_crc() は、読み込んだバッファ長を返す。remainder がその値で、既に > 図示してある。encoded_origsize は、ソースを見ると、元のファイルのサイ > ズを表すためだけの変数のようだ。以降は無視しよう。 > > ところで、ファイルサイズが小さい場合図の通りにならないっと考えるかも知 > れない。その通りなのだが、例外条件は少ない方がソースは理解しやすい。単 > 純な場合だけを考えた方が、あれこれ考えをめぐらす必要がないからだ。なに > しろ既に動くソースを見ているのだから、細かいことに目をつぶってもエンバ > グすることはないのである。そういうわけで、当面はこの図が唯一の初期状態 > であると考える。 > > (B) の部分はもう少し注目すべき箇所がある。 > > matchlen = THRESHOLD - 1; > > matchlen は、「一致した文字列長」であると予想したが THRESHOLD の値は 3 > (固定値)であるから、matchlen の初期値は 2 だ。いきなり予想がはずれた気 > がする。予想を立て直そう。2 という謎な数値と match*len* について考える > と、冒頭で のペアの len は 2 であることはないと説明した。無 > 意味だからであるが、matchlen の初期値はこの 2 と関連するかもしれない。 > そこで、matchlen の用途を以下のように予想しなおすことにする。以下のよ > うにメモを更新しよう。THRESHOLD(threshold は閾値の意)も一緒に予想した。 > > ---------------------------------------------------------------------------- > * matchlen 最低限一致しなければならない長さ-1 > * THRESHOLD 最低限一致しなければならない長さ > ---------------------------------------------------------------------------- > > うーん、本当だろうか? > > (B) の残りの部分を片付けよう > > pos = dicsiz; > > if (matchlen > remainder) matchlen = remainder; > > pos が dicsiz であることからどうやら、pos は、text[] のインデックスら > しい。前の予想で pos は、prev[] のインデックスでもあり、hash[] の値で > もあると予想したのだが(これはもちろん間違いではなかろうが)。どうやら > 本当の意味は、処理するテキストの先頭を示しているのではないかとも思える。 > まあ、ここでは無難に「text[] のインデックス(でもある)」とだけ理解しよう。 > 既に図には書き込んである。 > > 次の if だが、remainder が matchlen よりも小さい場合の条件だ。また、 > matchlen の予想が覆されそうな予感がしないでもないが、この if 文は*例外 > 条件*なので無視することにした。都合の悪いことは見ない方が良いのだ。 > > /* (C) */ > hval = ((((text[dicsiz] << 5) ^ text[dicsiz + 1]) << 5) > ^ text[dicsiz + 2]) & (unsigned)(HSHSIZ - 1); > > (C) である。これは難解である。複雑な数式は苦手であるが、じっくり考えよ > う。まず求める値は hval である。これは hash[] のインデックスなのだが、 > このような複雑な式で求まるインデックスなんて想像もつかない。まず、最初 > のインスピレーションを大事にすることにしよう。冒頭で、(C) の処理は「ハッ > シュ値 hval を計算する。」っと苦もなく予想した。そしてこれは間違いでは > ないだろう(きっと)。hash[] との関連をここで考えてもわからないから、こ > のハッシュ値の計算だけを考えることにしよう。 > > 式をじっくり見てみる。。。じっくり見てみると以下のことがわかる。 > > x(i) = text[dicsiz + i] > とすると > hval = (( x(0) << 5 > ^ x(1) ) << 5 > ^ x(2) ) > & (unsigned)(HSHSIZ - 1); > > である。演算子 << は、演算子 ^ よりも優先順位が低いので余計な括弧は省 > 略した。最後の & (unsigned)(HSHSIZ - 1) は、前にも似たような式が出たが > これはある範囲の数値(ここでは、0 〜 HSHSIZ{2^15}-1)を抽出するためのビッ > トマスクである。ハッシュ関数と言うのはある符号をある集合の符号に写像す > る関数であるからこのようなビットマスクは当然必要だし、良く行われる事だ > (普通は mod 素数を行うんだけど)。また、hval は、hash[] のインデックス > なのだから、写像する集合とは hash[] のインデックスだ。おっ、案外簡単に > わかった。x(i) が text[dicsiz + i] で、ハッシュ関数の変数は x(0), > x(1), x(2) だから、先頭の 3 バイトの文字列(平文)のハッシュ値を求めてい > るわけだ。その他の計算(<< 5 とか ^ とか) は大したことではない。無視し > よう。また、続けて (D) の処理も見るが、 > > /* (D) */ > insert(); > > insert() は、幸い解読ずみである pos を hash[] に格納する処理だ。 > 予想の段階では、(C) と (D) を別個の処理と考えていたのだがこれは > どうやらセットである。 > > (C) pos の位置の 3 文字のハッシュ値を計算し > (D) hash[ハッシュ値] = pos を行う > > もう少し注意深く検討すると「posの位置の3文字」と、求めた「ハッシュ値」 > は論理的には = である。 > > つまり、(C) (D) の処理は > > hash[文字列] = 位置 > > という処理を行っている。ハッシュ値の衝突はここでは考えない。slide 辞書 > 法では、ある文字列に対し以前その文字列が現れたかどうかを検索し、その位 > 置を求める必要があるのだが、この最初の 3 文字に関しては現時点でその用 > 件(位置を求める)を満たす事ができている。ここまでで自ずと encode() の全 > 体像も予想がつきそうな気がする。 > > 衝突は考えないっとしたが、ちょっと考えたらすぐわかった。prev[] には、 > 以前のハッシュ値で求めた文字列の位置が入っている。つまり、prev[] はハッ > シュが衝突したときのためのバッファだ。このハッシュはチェーン法だ。 > > 例えば、insert() で、 > prev[pos] = hash[hval]; > hash[hval] = pos; > っと処理をしているのだから > > hash[hval] = pos1 > | > v > prev[pos1] = pos2 > | > v > prev[pos2] = pos3 > ... > > といった値が入る事になる。ある文字列(のハッシュ値) hval に対して、その > 辞書上の位置は pos1, pos2, pos3 という候補があるわけだ。実際にどの pos > を選ぶかは比較によって行われるのだろう。 > > # それにつけても、(C) と (D) の部分を見るだけでもこのソースがちょっと > # 汚いことがわかる。もう少し、引数とか戻り値とか考えてくれても良かっ > # たはずだ。ハッシュ関数にしても少なくともマクロぐらいにはしようよ。 > > (E) 〜 (H) に移ろうこれはループの中身で、処理の本題だ。まずループの脱 > 出条件を見てみると > > while (remainder > 0 && ! unpackable) { > > remainder は、バッファ上に読み込んだ平文の長さであるからこれがなくなる > までループすることになる。さらに unpackable というのは、crcio.c の > putcode() でその値を設定している箇所が出て来るのだが、符号化した出力サ > イズが元のサイズを越えたときに真になる。つまり、これ以上処理しても圧縮 > の意味がないとわかったらループを抜けるわけだ。 > > では、(E)を見よう。 > > /* (E) */ > lastmatchlen = matchlen; lastmatchoffset = pos - matchpos - 1; > --matchlen; > > ちょっと見ただけではやはりわからない。これらの変数はまだ予想しかしてな > いからである。が、わかるだけの情報は書きしるそう。 > > ---------------------------------------------------------------------------- > * lastmatchlen 以前の matchlen の値 (変数名から) > * lastmatchoffset 以前マッチした位置 (変数名から) > ---------------------------------------------------------------------------- > > 以前の値をlast〜に退避し、新たな値を設定する準備をしているわけだ。そし > て、「新たな値の設定」は、--matchlen で早速行われている。しかし、「マッ > チした長さ」をまだ何もしてないのに -1 するというのはいったいどういうこ > とだろう? matchlen はループの頭で 2 に設定されている。これが 1 になっ > た。本当の初期値は 1 なのか? > > ---------------------------------------------------------------------------- > < 各変数の初期値 > > > matchlen = 1 > matchpos = 0 > pos = dicsiz > > lastmatchlen = 2 > lastmatchoffset = dicsiz - 1 (pos - matchpos - 1) > ---------------------------------------------------------------------------- > > この (E) はまた後で見る事になるだろう。 > > (F) (G) である。また、その直後には以前にも見た境界条件がある。 > > /* (F) */ /* (G) */ > get_next(); match_insert(); > if (matchlen > remainder) matchlen = remainder; > > if 文 は無視して関数の中身だけを追いかけてみよう。まず、get_next() こ > れは 次の token を取ってくる処理だと予想してある。はたしてどうだろうか? > > static void get_next() > { > remainder--; > if (++pos >= txtsiz - maxmatch) { > update(); > } > hval = ((hval << 5) ^ text[pos + 2]) & (unsigned)(HSHSIZ - 1); > } > > remainder を消費し、pos を進めている。予想通りだ。ひとまず if の条件は > 無視すると直後で hash 値を求め直している。このハッシュ関数は、以前のハッ > シュ値を利用しているが、これは pos が以前より + 1 されていることを考え > ると関連が見えて来る。以前のhash関数を pos の関数として書き直すと > > x(pos+i) = text[pos + i] > > hval(pos) = (( x(pos+0) << 5 > ^ x(pos+1) ) << 5 > ^ x(pos+2) ) > & (unsigned)(HSHSIZ - 1); > > であり、また、今度のハッシュ関数は、 > > hval(pos+1) = ( hval(pos) << 5 > ^ x(pos+1 + 2) ) > & (unsigned)(HSHSIZ - 1); > > だ、繁雑なので & (HSHSIZE-1) を外すと、 > > hval(pos+1) = (( x(pos+0) << 5 > ^ x(pos+1) ) << 5 > ^ x(pos+2) ) << 5 > ^ x(pos+3) > > っとなる。この次 get_next() が呼び出されれば、 > > hval(pos+2) = ((( x(pos+0) << 5 > ^ x(pos+1) ) << 5 > ^ x(pos+2) ) << 5 > ^ x(pos+3) ) << 5 > ^ x(pos+4) > > である。順にハッシュ値を求める文字列長を増やしている。とにかく、 > get_next() は、pos を進め、remainder を縮め、新たな(以前より1文字長い) > 文字列のハッシュ値 hval を求める関数のようだ。 > > しかし、いつまでも hash 値の元となる文字列を伸ばしてもしょうがないだろ > う。hval はどこかでまたリセットされるはずだ。っと思ってソースを探して > みたがそのような箇所は見当たらない。なぜだろう?考えてみる・・・最初は > わからなかったが数式をよく見てみたらわかった。<< 5 が鍵だ、hval(pos+2) > の式を見ると x(pos+0) は、<< 5 が、4回も行われているつまり、20ビットの > シフトだ。hval(pos+3) なら、25ビット、hval(pos+4) なら 30 ビットのシフ > トだ。さすがにこれだけシフトすれば、x(pos+0)の情報は消えてもいいだろう。 > > 実際、hval は何文字分の情報を持つのだろう?hval は、unsigned int で、 > 普通 32 bit であるから、6.4 文字分だろうか?いや、実際にはハッシュ値の > 計算時にHSHSIZ (15bit) でマスクをかけているから 15 bit の情報しか持た > ない。つまり、3文字だ。ビット計算は苦手なので図示して確認しよう。 > > 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 > +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ > hval |--| | | | | | | | | | | | | | | | > +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ > > 最初の hval(0) は、x(0), x(1), x(2) に対して、 > > <--- 5 -----> <--- 5 -----> <--- 5 -----> > +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ > x(0) <<10 -- x x x x x > +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ > x(1) << 5 -- x x x x x x x x > +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ > x(2) -- x x x x x x x x > +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ > > の排他的論理和である。hval(0) の時点で x(0) の情報は 5 ビット残ってい > るが hval(1) になれば消えてしまうのは自明である。どうにも最初の文字に > 関しては 5 ビットしか情報を使用しないと言うのが気持悪いのだが、15 bit > サイズの変数 hval には、過去 3 文字分の情報しか保持されないのは間違い > ない。get_next() の処理を見れば、位置 pos に対して、hval は常に pos, > pos+1, pos+2 の情報しか持たないわけだ。これは重要だ。メモしよう そうです.これは余り多く領域を取ってもあまり効果が 無いし,それよりも当初のMS-DOSでは都合のいい値だった ためです.で,最小一致が3なのは? > ---------------------------------------------------------------------------- > * hval hash[]のインデックス。現在位置 pos に対して、 > text[pos], text[pos+1], text[pos+2] のハッシュ値で、論理的には > hval == text[pos] + text[pos+1] + text[pos+2] > と同義 > ---------------------------------------------------------------------------- > > ところで、前回、hval の計算とinsert() はセットだと言った。今回はどうだ > ろう?次の match_insert() を見てみる。 > > static void match_insert() > { > ... 省略 ... > > prev[pos & (dicsiz - 1)] = hash[hval]; > hash[hval] = pos; > } > > ・・・強敵であった。強敵すぎたので逃げて、最後の2 行だけに着目した。こ > れは、insert()と同じだ。予想は当たっている。get_next() で hval を更新 > した後は、この match_insert() で、prev[] と hash[] を更新するわけだ。 > そして、match_insert() の省略した部分は、どうやら matchpos, matchlen, > too_flag を更新しているだけのようだ。これが本当なら match_insert()で、 > insert()の処理をせず、関数を分けるかした方が良さそうだ。(真偽の程は詳 > 細を見てからになる) > > おもむろに後続の処理 (H) を見ると、 > > /* (H) */ > if (matchlen > lastmatchlen || lastmatchlen < THRESHOLD) { > > これが真なら「見つからなかった状態」と予想した(なぜだろ?)。そして、 > lastmatchlen は初期状態では 2 である。予想した条件は逆か? matchlen ま > わりは予想ばかりで進めすぎた。そしてどうやら match_insert() を読みとか > なければこの先も分からずじまいになりそうだ。 > > このまま match_insert() を詳細に解析する事にしよう。match_insert() > をすべて再掲する。 > > /* 現在の文字列と最長一致する文字列を検索し、チェーンに追加する */ > > static void match_insert() > { > unsigned int scan_pos, scan_end, len; > unsigned char *a, *b; > unsigned int chain, off, h, max; > > max = maxmatch; /* MAXMATCH; */ > if (matchlen < THRESHOLD - 1) matchlen = THRESHOLD - 1; > matchpos = pos; > > off = 0; > for (h = hval; too_flag[h] && off < maxmatch - THRESHOLD; ) { > h = ((h << 5) ^ text[pos + (++off) + 2]) & (unsigned)(HSHSIZ - 1); > } > if (off == maxmatch - THRESHOLD) off = 0; > for (;;) { > chain = 0; > scan_pos = hash[h]; > scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; > while (scan_pos > scan_end) { > chain++; > > if (text[scan_pos + matchlen - off] == text[pos + matchlen]) { > { > a = text + scan_pos - off; b = text + pos; > for (len = 0; len < max && *a++ == *b++; len++); > } > > if (len > matchlen) { > matchpos = scan_pos - off; > if ((matchlen = len) == max) { > break; > } > } > } > scan_pos = prev[scan_pos & (dicsiz - 1)]; > } > > if (chain >= LIMIT) > too_flag[h] = 1; > > if (matchlen > off + 2 || off == 0) > break; > max = off + 2; > off = 0; > h = hval; > } > prev[pos & (dicsiz - 1)] = hash[hval]; > hash[hval] = pos; > } > > まず、初期化部分の前半 > > max = maxmatch; /* MAXMATCH; */ > if (matchlen < THRESHOLD - 1) matchlen = THRESHOLD - 1; > matchpos = pos; > > off = 0; > > maxmatch は、固定値で 256 だ、だから max も 256 > 2行目の if 文は、これまでしつこいくらいに出て来た条件に似ているが、今 > 回は条件を満たすらしい。これまでは、 > > if (matchlen > remainder) matchlen = remainder; > > という条件だった。そして今回は、 > > if (matchlen < THRESHOLD - 1) matchlen = THRESHOLD - 1; > > だから、全体的に matchlen の値は、 > > THRESHOLD-1 <= matchlen <= remainder > > つまり、 > > 2 <= matchlen <= バッファに残ったテキスト長 > > の範囲に納められるようだ。ここでは、matchlen は下限値を下回るので2 に > 設定される。次に matchpos, off が初期化され。以下の図の状態になる。 > (pos, remainder は、get_next() で更新されていることに注意) > > ---------------------------------------------------------------------------- > > dicsiz=2^dicbit 2*2^dicbit > v v txtsiz > +-------------+-------------+-------------+-------------+---+ > text | | | | | | > +-------------+-------------+-------------+-------------+---+ > `-pos(=dicsiz+1) <---> > matchpos(=pos) maxmatch{256} > off(=0) > > <------ remainder ------------> > > |--- この位置に最初の ---------| > データが読まれている > ---------------------------------------------------------------------------- > > 初期化部分の後半 > > for (h = hval; too_flag[h] && off < maxmatch - THRESHOLD; ) { > h = ((h << 5) ^ text[pos + (++off) + 2]) & (unsigned)(HSHSIZ - 1); > } > if (off == maxmatch - THRESHOLD) off = 0; > > h は、too_flag[] が今のところすべて0だから hval だ。(too_flag は、h つ > まり hval をインデックスに取るらしい。hash[] と同じだ。再掲はしないが > メモに書き加えておこう) > > off は、pos の位置からのオフセットのようだ(h を更新する for 文の中身か > ら)。図もその位置に書いた。最後の if 文は off が上限に達した場合に0 に > 再初期化している。よくわからないので無視しよう。for 文の中身からh や > off の用途はどうも先読みしたハッシュ値とその先読みの位置なのではないか > と想像する。too_flag[] の状態によって先読みすべき値が変わるのだろうか? > > とにかく処理の本題に入る事にしよう。まず、この関数に現れる局所変数を列 > 挙しておこう > > unsigned int scan_pos, scan_end, len; > unsigned char *a, *b; > unsigned int chain, off, h, max; > > off, h, max はすでに出て来たので残りは > > scan_pos, scan_end, len, a, b, chain > > だ、これだけの変数の意味を解読しなくてはならない。変数は状態を表すから、 > その数が多いと言うのはそれだけ複雑な処理だということだ。めげる。 > > この関数のメインとなるループの中をざっと眺めてみるさらに内部にループが > ある。ひとまず、二重ループの中身を省略しよう。 > > for (;;) { > chain = 0; > scan_pos = hash[h]; > scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; > > while (scan_pos > scan_end) { > chain++; > ... 略 ... > } > > if (chain >= LIMIT) > too_flag[h] = 1; > > if (matchlen > off + 2 || off == 0) > break; > max = off + 2; > off = 0; > h = hval; > } > > まず、前半部分から > > chain = 0; > scan_pos = hash[h]; > scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; > > chain, scan_pos, scan_end はすべて while ループに渡されるべき変数だ。 > さらに、while の後には、scan_pos, scan_end は現れないから(仮に while > ループが1つの関数だったとすると)これらは while ループ部の引数(入力)だ。 > この2つの変数はどうやりくりしようとも、while ループ部内の状態しか表さ > ないので、ここでは無視しよう。 > > while ループの後を見てみると > > if (chain >= LIMIT) > too_flag[h] = 1; > > if (matchlen > off + 2 || off == 0) > break; > max = off + 2; > off = 0; > h = hval; > > chain が LIMITを越えた場合、too_flag[h] = 1 としている。chain は、ざっ > と見て、while ループのカウンタらしいが、LIMIT は 0x100 だ。どうにも例 > 外条件っぽい(LIMITという名前や数値がそう思わせる)のでここでは無視しよ > う。while ループが 256以上回る可能性がある点だけ心にとどめておこう。 > > 次の条件では、matchlen と off が条件判断されている。ということはこのど > ちらか、あるいは両方は while ループの返り値(出力)だ。ざっと > match_insert()全体を見てみると off は最初とこの直後でしか更新されない > らしい。つまり、while ループ部の返り値はmatchlen の方だ。 > この条件は for () ループの脱出条件でもある。心にとどめて、次に進む。 > > max = off + 2; > off = 0; > h = hval; > > ふむ。よくわからない。しかし注目すべき点はある。off はここで、0 になる > がこれ以降は off の値は変わらない。つまり、off は最初は何らかの値で > while ループ部に渡されるが、その次からは、0 だ。この for ループが何度 > 回ろうとも 0 だ。h も同じで最初は何らかの値を持つが、2回目のループ以降、 > h は hval だ。max は、off を 0 にする直前に更新しているから、h や off > と事なり、3つの状態を持つ、すなわち。maxmatch, off+2, 2 だ。 > > いや、脱出条件を見てみると off == 0 なら break とある。つまり、この > for ループはどんなに頑張っても2回しか回らないらしい。やっぱり max も 2 > つの状態しか持たないようだ。 > > これで、1 回目、2回目に while ループ部に入る直前の状態が書ける。この関 > 数 match_insert() は、while ループ部を1回か2回実行する処理と言うわけだ。 > > ここで無視していた。while ループ部の入力となる scan_pos, scan_end > もそれぞれどのような状態になるか書いておく > > ---------------------------------------------------------------------------- > < 1回目 > > h = 何か > off = 何か > max = maxmatch > > scan_pos = hash[h] > scan_end = pos + off - dicsiz (あるいは、off) > > matchlen = 2 > matchpos = pos > < 2回目 > > h = hval > off = 0 > max = 前の off + 2 > > scan_pos = hash[hval] > scan_end = pos - dicsiz (あるいは、0) > > matchlen = ? > matchpos = ? > ---------------------------------------------------------------------------- > > 上記は一般化した場合だが、今回(初回)の場合、h や off の値は、hval であ > り、0 だった。2回目ループのときの状態と同じである。2回のループの違いは > max の値がmatchpos であるか off+2 (すなわち2)であるかの違いしかないようだ。 > > ここは、条件を少なくするためにこの場合だけにしぼって処理を考えよう。 > while ループの2回の呼び出しを行う際の状態は以下の通りに書き直せる。 > > ---------------------------------------------------------------------------- > < 1回目 > > h = hval > off = 0 > max = maxmatch > > scan_pos = hash[hval] > scan_end = pos - dicsiz (あるいは、0) > > matchlen = 2 > matchpos = pos > < 2回目 > > h = hval > off = 0 > max = 2 > > scan_pos = hash[hval] > scan_end = pos - dicsiz (あるいは、0) > > matchlen = ? > matchpos = ? > ---------------------------------------------------------------------------- > > うーん、まだ、すっきりしない。何がすっきりしないかというと scan_end の > 値だ。これが何を意味するのかがよくわからない。scan_pos は、わかるのか? > というと、わかる。hash[hval]だから現在の文字列と同じ文字列の辞書上の位 > 置だ。さらに、現時点では get_next() で、hval を更新してから insert() > を行っていないので、hash[hval] には何も入っていない。すなわち 0 だ。 > > scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; > > を考えよう。off は、0 だから > > scan_end = (pos > dicsiz) ? pos - dicsiz : 0; > > なわけだ。さらに、posは現時点で dicbit+1 であるから、1 だ。図に書こう。 > > ---------------------------------------------------------------------------- > > dicsiz=2^dicbit 2*2^dicbit > v v txtsiz > +-------------+-------------+-------------+-------------+---+ > text | | | | | | > +-------------+-------------+-------------+-------------+---+ > ^ ^ `-pos(=dicsiz+1) > | | > | scan_end はこの辺(1) > scan_pos はこの辺(0) > > h = hval > off = 0 > max = 2 > > ---------------------------------------------------------------------------- > > ついに、text[] バッファの左半分に指しかかる。これが何なのかは現時点で > は明確に書いてなかったが予想するとこの左半分はズバリ辞書だ。言い切って > やろう。今まで辞書らしい(dicsizのサイズを持つ)バッファは hash[] や > prev[] があったが、hash[], prev[] の用途はもう明確である。辞書となり得 > るバッファはもうこの text[] しかないのだ。 > > さらに、左半分に限らずこの text[] 全体が辞書であろうと予想する。これは > ただの勘だが text[] は環状バッファなのではなかろうかと考えている。 > > # 最初の方で prev[] が辞書だと予想したが間違った予想をしていたことにこ > # の時点で気づいた。prev[] が辞書と同じサイズを持つ理由はまだよくわか > # らない。 > > この時点ではまだ scan_pos や scan_end の真の意味はわからない。off のこ > とを無視しているから予想も立ちにくいが、ひとまず初期状態がどういったも > のかはわかったのでこのまま、while ループ部を見てみたいと思う。 > > while (scan_pos > scan_end) { > chain++; > > if (text[scan_pos + matchlen - off] == text[pos + matchlen]) { > { > a = text + scan_pos - off; b = text + pos; > for (len = 0; len < max && *a++ == *b++; len++); > } > > if (len > matchlen) { > matchpos = scan_pos - off; > if ((matchlen = len) == max) { > break; > } > } > } > scan_pos = prev[scan_pos & (dicsiz - 1)]; > } > > まず、if 文の条件を満たさない場合だけを考える。 > > while (scan_pos > scan_end) { > chain++; > > if (text[scan_pos + matchlen - off] == text[pos + matchlen]) { > ... > } > scan_pos = prev[scan_pos & (dicsiz - 1)]; > } > > > offは 0 なので、text[scan_pos + matchlen] != text[pos + matchlen] という条件の場合を想定するわけだが、 > > text[scan_pos + matchlen] > > と > > text[pos + matchlen] > > を比べている > > text[scan_pos] 辞書上の文字列の*先頭* > text[pos] 現在の文字列の*先頭* > > を比べないのは matchlen が前に予想した「最低限一致しなければならない長さ-1」 > だからであろう。現時点で、matchlen が 2 だから > > text[scan_pos + 0] == text[pos + 0] > text[scan_pos + 1] == text[pos + 1] > > であったとしても、 > > text[scan_pos + 2] != text[pos + 2] > > であれば、「最低限一致しなければならない長さ」という条件を満たさないの > である。なので matchlen の位置から先に比較して無駄な比較をしないように > している。後でちゃんとした比較の処理が出て来るだろう。このような処理は > 処理としては効率が良いのだが、ことソース理解と言う点では冗長である。わ > かりにくいのだ。仕方ないのだけど。 > > # matchlen の意味の予想はどうやら当たっているようだ。matchlen は最短一 > # 致長で、minmatchlen っと名付けても良さそうな変数だ。 > > そして、比較に失敗したら scan_pos を更新する。 > > scan_pos = prev[scan_pos & (dicsiz - 1)]; > > ハッシュのチェーンをたどっている、つまり次の候補を辞書から取り出してい > るわけだ。ここまでで、while ループの処理内容の想像はついた。このループ > は辞書から(最長)一致する文字列を探しているのだろう。 > > 順番が前後したが、while ループの脱出条件を見てみる > > while (scan_pos > scan_end) { > > これはどういうことだろう? scan_pos は、ハッシュのチェーンをたどって同 > じハッシュ値を持つ文字列の位置を探す、この値はだんだんと小さくなって行 > くものなのだろうか? > その通りである。hash[] への格納はファイルから取って来た文字列を順に格 > 納して行くのでチェーンの奥には、より前の方の位置が書かれているはずだ。 > 逆にチェーンの浅い部分にはより現在位置に近い、位置が書かれているのだろ > う。では、その境界 scan_end はどうやってわかるのだろうか?これは後でま > た検証しよう。 > > では、処理の本質 if 文の中を見る事にしよう > > if (text[scan_pos + matchlen - off] == text[pos + matchlen]) { > { > a = text + scan_pos - off; b = text + pos; > for (len = 0; len < max && *a++ == *b++; len++); > } > > if (len > matchlen) { > matchpos = scan_pos - off; > if ((matchlen = len) == max) { > break; > } > } > } > > 最初の意味もなくブロックになっている部分を見る、 > > { > a = text + scan_pos - off; b = text + pos; > for (len = 0; len < max && *a++ == *b++; len++); > } > > この処理では a, b といういかにも局所な名前の変数が使われている。これは、 > 本当にこのブロック内にで局所的なもののようだ。ならば定義位置もこのブロッ > ク内にして本当に局所的にして欲しかった。 > > さらに、この処理は単に文字列 a, b を比較しているだけのようだ。memcmp() > ではまずいのかと言うとここで求めているものが「どこまで一致したか(len)」 > のようなので、memcmp() では役不足だ。仕方ないので関数をでっちあげて抽 > 象化をはかろう。memcmp_ret_len()(我ながら変な名前だ)という関数があった > とするとこの部分は > len = memcmp_ret_len(&text[scan_pos-off], &text[pos], max); > > っとなる。返り値は一致した文字列長だ。 > > その次の処理、 > > if (len > matchlen) { > matchpos = scan_pos - off; > if ((matchlen = len) == max) { > break; > } > } > > で、matchlen (最低一致長)より大きい場合に条件を満たす。条件を満たさな > ければ、scan_pos を更新し、次のループに移る。では、条件を満たす場合を > 見てみよう。まず最短一致長の一致条件を満たした場合、matchpos と > matchlen を更新する。 > > matchpos はマッチした位置、 > matchlen はマッチした長さ > > で、matchlen が max なら最長一致長に達しているので、これ以上探索はしな > い。matchlen は最短一致長でありながら、一致長でもある変数のようだ。 > (どうやら以前の2つの予想はどちらも当たっていた模様) > > とにかく while ループ部の出力は、この matchpos と matchlen のようだ。 > 前に書いた通りこのループは「最長一致文字列を求める処理」だ。 > > match_insert() の全体をもう一度見てみよう。ただし、以下の書き換えを行う。 > > o while ループ部は search_dict(pos, scan_pos, scan_end, max) という関数 > に置き換えたものとする。 > > o 末尾の insert() と同等の処理を行っている部分も insert() の呼び出しに > すり替えよう。(match_insert() 関数の中で insert() 処理を本当に行うべ > きものなのかどうかが疑問だが) > > o chain という変数にかかわる処理も隠した(search_dict内で行う) > > o for ループは、2回しかまわらないので、2 度の search_dict() の呼び出し > に書き換える > > static void match_insert() > { > unsigned int off, h, max; > unsigned int scan_end; > > max = maxmatch; /* MAXMATCH; */ > if (matchlen < THRESHOLD - 1) matchlen = THRESHOLD - 1; > matchpos = pos; > > off = 0; > for (h = hval; too_flag[h] && off < maxmatch - THRESHOLD; ) { > h = ((h << 5) ^ text[pos + (++off) + 2]) & (unsigned)(HSHSIZ - 1); > } > if (off == maxmatch - THRESHOLD) > off = 0; > > scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; > search_dict(pos, hash[h], scan_end, maxmatch); > > if (matchlen <= off + 2) { > off = 0; > h = hval; > > scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; > search_dict(pos, hash[h], scan_end, off+2); > } > > insert(); > } > > だいぶすっきりした(が、まだ繁雑だ)。まだ、off にかかわる部分がよく分か > らないが、ひとまず良いだろう。この関数の解析はこれで終って良いだろうか。 > > いや、良くない。肝心の match_insert() の出力がよくわからない。この関数 > は「最長一致文字列を探し、hash を更新する処理」(くどいようだが、hashを > 更新するは余計に思う)なのだろうが、最長一致文字列が見つからなかったと > いうのはどう判断されるのだろう? > > まず、search_dict() で見つからなかった場合、matchlen, matchpos は更新 > されない。そして、おそらく 2 度目の search_dict() の呼び出しが行われる。 > が、too_flag[] というので、判断できそうな気もするがこれはむしろハッシュ > のチェーンをたどりすぎるのを止めるためのフラグであるように思える。 > > 2度目の search_dict()で、max の値が変わるのが鍵だろうか?。今回の場合、 > max は 256 から 2 になる。最長一致長として 2 が限界値になると、 > search_dict() の動作は変わるだろうか?いや、やはり変わらない。どうにも > この関数だけでは見つかったか見つからなかったかという判断はできないよう > だ。(本当はわかっているはずなのにその情報を直接外に持ち出していない) > > 気持悪いがやはりこの関数の解析を終え、次に移る事にしよう。 > > (H) である。以前、 > > (H) matchlen > lastmatchlen || lastmatchlen < THRESHOLD なら > > (H.1) output() する。(マッチしなかったらそのまま出力しているのだろう。たぶん) > (H.2) そうでなければ(マッチしたなら)、output()し、何かいろいろする。 > > っと予想した部分だ。今や match_insert() は、解析済みだからこれの真偽が > わかるか?というとやっぱり、わからない。ただ、 > matchlen > lastmatchlen > というのは、辞書から文字列が見つかった場合の条件になりそうだから、やはり > これは予想が逆だろうか?とにかく、比較的簡単そうな、(H.1) から見よう。 > > if (matchlen > lastmatchlen || lastmatchlen < THRESHOLD) { > /* (H.1) */ > encode_set.output(text[pos - 1], 0); > count++; > } else { > > どうも、文字 text[pos-1] を出力しているだけのように思える。文字の出力 > は、slide 辞書法では「辞書から見つからなかった場合」を意味するから、や > はり最初の予想はあってそうなのだが・・・仕方ないので、output()の処理を > 除いて見よう。これは、lh5, 6, 7 の場合、huf.c:output_st1(c, p) である。 > 現時点で処理の内容を見てもわけがわからないが、結論から言うと第一引数 c > は、文字で、第二引数 p は、位置である。冒頭の decode 処理で、文字 c は > 長さも兼ねていると説明済みなので、(そして、text[pos-1] には現時点で文 > 字そのものしか書かれていない)これはやはり文字を出力している処理だ。つ > まり「見つからなかった場合」の処理だ。 > > なぜ、pos-1 なのだろう?確かに Huffman coding に文字を渡すのはこれが初 > めてで、現在 pos の位置はバッファの1文字進んだ位置にある。pos-1 は出力 > しなければならないのは当然だ。ということは pos は常に「未出力文字の位 > 置 + 1」なのかもしれない。 辞書を検索してtext[pos-1]よりもtext[pos]から比較した方が長く 一致するケースがあったため,2回検索して長く一致したら,先に text[pos-1]で辞書に一致したとしてもそれを使わずに長いほうを 選ぶということをしたらしいです. > 次の count++ を見る。count はどうやらこの関数の変数ではないらしい、困っ > た事に局所変数の名前っぽいがグローバル変数だ。これはよろしくない。ちょっ > と grep しただけでは、他にどこでこの変数を使っているのかわからなかった。 > まあ、今 1 文字を渡した所なので、入力文字数なのだと仮定しておこう。こ > の変数が大勢に影響を与える事はないだろうからこれ以上は見ないと言うのも > アリだ。 > > 次は (H.2) である。これがまた難解なのだがゆっくり片付けよう。 > > } else { > /* (H.2) */ > encode_set.output(lastmatchlen + (UCHAR_MAX + 1 - THRESHOLD), > (lastmatchoffset) & (dicsiz-1) ); > --lastmatchlen; > > while (--lastmatchlen > 0) { > get_next(); insert(); > count++; > } > get_next(); > matchlen = THRESHOLD - 1; > match_insert(); > if (matchlen > remainder) matchlen = remainder; > } > > まず、output() に渡している引数は、それぞれ「長さ」と「位置」であろう > ことは予想済みだ。さらに UCHAR_MAX{255} + 1 - THRESHOLD{3} だから > > 長さ lastmatchlen + 253 > 位置 lastmatchoffset & (dicsiz-1) > > となっている。冒頭の decode() の解析で、長さは 253 を足す事は確認済み > だ(ふと -lhs- の場合 254 を足すという動作が、encoding 部分では考慮され > ていないようだと気づく。いいのかそれで?)。ところで、一致長 > lastmatchlen は 3 以上で初めて 255 を越えることができる。以前予想した、 > THRESHOLD の意味「最低限一致しなければならない長さ」はあっているらしい。 > > もう一点、注意しなくてはならないのは、出力しているのが lastmatchlen と > lastmatchoffset である。これらは、match_insert() のときには更新してい > ない(last〜の更新は次のループの先頭 (E) で行われる)。先程 (H.1) のとき > も書き出していたのは、text[pos-1] であった。pos 位置は一歩先読みした位 > 置を指すらしい。このような処理を行う場合、最後に調整が必要なはずだ(で > ないと最後の文字が出力されない)。その調整はどこで行われるのだろう? > > さて、後続の処理だが、<長さ、位置>のペアを出力した後は、 > > --lastmatchlen; > > while (--lastmatchlen > 0) { > get_next(); insert(); > count++; > } > > という処理を行っている。get_next() は、pos を進める処理、insert() は辞 > 書に登録する処理だから、これは文字列を読み飛ばしている処理だ。確かに > lastmatchlen 分の情報は出力済みだから、これは納得である。lastmatchlen > を 1 余分に引いているのが謎だがこれは pos が一歩先に進んでいるからであ > ろうと予想する。つまり、この後は pos の位置はまた「現在位置」に戻る。 > なるほど、先程調整が必要と書いたがここで行われているらしい。細部は不明 > だが少なくとも辞書に文字列が見つかった場合は最後まで出力されるようだ。 > > 次に進もう > > get_next(); > matchlen = THRESHOLD - 1; > match_insert(); > if (matchlen > remainder) matchlen = remainder; > > せっかく pos が現在の位置に戻ったのに、get_next() でまた先読みされた。 > うーむ。そして、matchlen は初期化される。一致情報はすでに出力済みだか > らこれはうなずける。そして、match_insert() が呼ばれる。この時点で再度 > 辞書が検索される。pos はまた1文字進んでいるのだから、これは先程(初期状 > 態)のmatch_insert() と大差ない処理だ。(その直後のif文は境界条件なので > 無視) > > そうして、また次のループに移る。このとき続けざま get_next(), > match_insert() が行われる。どうやら pos は次のループからは、 2 文字文 > 先に進んでしまうようだ。なぜだろう? > > どうにもソースを見ただけで解読するには、このあたりが限界のようだ。どう > しても細部がわからないし、事実が見えないから予想の積み重ねがたまって不 > 安になる。 > > 実は、もう少しマメに図を起こして読み進んで行けばもっとわかることがあっ > ただろうと思うのだが、それは面倒だし、間違える可能性がある(ここまでで > 何度か痛い思いをした)。以降は、いくつかのデータを実際に圧縮させその動 > きをデバッガで追うことで、これまでの解析結果を検証してみよう。 > > ・・・っと、その前に、ここまでですべての関数を網羅してしまったと思って > たのだが、一つ忘れていたものがあった。update() だ。この関数は、 > get_next() で呼び出されていたのだが、以前は無視していた。先にここを見 > ておこう。 > > まず、get_next() を再掲する。 > > static void get_next() > { > remainder--; > if (++pos >= txtsiz - maxmatch) { > update(); > } > hval = ((hval << 5) ^ text[pos + 2]) & (unsigned)(HSHSIZ - 1); > } > > remainder と pos を進めた後、pos が txtsiz - maxmatch に達してしまった > 場合(pos == 2 * 2^dicbit の場合)に呼び出されるようだ。つまり、以下の図 > の状態だ。これが、update() を呼び出す時の初期状態だ。 > > ---------------------------------------------------------------------------- > > dicsiz=2^dicbit 2*2^dicbit > v v txtsiz > +-------------+-------------+-------------+-------------+---+ > text | | | | | | > +-------------+-------------+-------------+-------------+---+ > /<---> > / maxmatch{256} > pos > > <--> > remainder > > ---------------------------------------------------------------------------- > > では、update() に入る。 > > static void update() > { > unsigned int i, j; > unsigned int k; > long n; > > #if 0 > memmove(&text[0], &text[dicsiz], (unsigned)(txtsiz - dicsiz)); > #else > { > int m; > i = 0; j = dicsiz; m = txtsiz-dicsiz; > while (m-- > 0) { > text[i++] = text[j++]; > } > } > #endif > n = fread_crc(&text[(unsigned)(txtsiz - dicsiz)], > (unsigned)dicsiz, infile); > > remainder += n; > encoded_origsize += n; > > pos -= dicsiz; > for (i = 0; i < HSHSIZ; i++) { > j = hash[i]; > hash[i] = (j > dicsiz) ? j - dicsiz : NIL; > too_flag[i] = 0; > } > for (i = 0; i < dicsiz; i++) { > j = prev[i]; > prev[i] = (j > dicsiz) ? j - dicsiz : NIL; > } > } > > 先頭で、なぜか memmove() を for ループで書き換えている。なぜこのような > ことを行っているのだろう。for ループを見てみてもやっていることは変わら > ない。謎だが、とにかく、text[] の右半分(maxmatch 部分も含む) を左に移 > している。 多分ここを触ったのは私です. > 次に fread_crc() で、新たにファイルを読み込む。今度の読み込み位置は > &text[txtsiz - dicsiz] で、長さは dicsiz である。当然、remainder も更 > 新している。encoded_origsize は以前と同様無視。pos は dicsiz 分減らさ > れている。これはつまり図示すると、以下の状態になると言う事だ > > ---------------------------------------------------------------------------- > > dicsiz=2^dicbit 2*2^dicbit > v v txtsiz > +-------------+-------------+---+---------+-------------+---+ > text | | | | | | | > +-------------+-------------+---+---------+-------------+---+ > /<---> <---> > / maxmatch{256} maxmatch{256} > pos > > <------------------------------> > remainder > |------- 以前のデータ ---------|--- 新しいデータ ---------| > > ---------------------------------------------------------------------------- > > 以降、ファイルの読み込みは常にこの update()でしか行われない。pos は、 > 初期状態と同じ位置なので、初期状態が再現されている。ここまでで、 > maxmatch の領域はなんだろうと思うが、おそらく先読みのためだろう。それ > らしい処理は、match_insert() の冒頭にあった(が、現時点で詳細には触れて > いない)。 > > update() の残りを見る。 > > for (i = 0; i < HSHSIZ; i++) { > j = hash[i]; > hash[i] = (j > dicsiz) ? j - dicsiz : NIL; > too_flag[i] = 0; > } > for (i = 0; i < dicsiz; i++) { > j = prev[i]; > prev[i] = (j > dicsiz) ? j - dicsiz : NIL; > } > > 内容は、想像がつくので詳細は省略しよう。単に以前のデータが移動したので、 > ハッシュ値を更新しているだけだ。しかし、これはなかなか無駄な処理だ。 > > 以前、text[] は環状バッファだろうと予想したが予想がはずれたことがわかっ > た。環状バッファにしていれば、このハッシュの書き換えは不要にできただろ > うと思うのだが・・・ > # そのかわり、位置の大小比較が繁雑にならないので、これはこれで良いのか > # もしれない。どちらが優れているかは実験してみなければわからないだろう。 これは辞書から消える文字列に該当するハッシュ値をハッシュリスト から消すためと思いました. 無くても大丈夫ですか? > これで、一応は slide.c を網羅する事ができた。まだ不明な点は多いが、デ > バッガで実際の処理を追いかければまたわかることがあるだろう。 > > > # Local Variables: > # mode : indented-text > # indent-tabs-mode: nil > # End: > On Mon, 06 Jan 2003 00:20:16 +0900 (JST) Koji Arai wrote: > 新井です。 > > 更新。この資料は、lha の CVS リポジトリに登録した。 > > Index: Hackinf_of_LHa ----------------------------------------------------- Tsugio Okamoto (E-Mail:tsugio @ muc.biglobe.ne.jp) From JCA02266 @ nifty.ne.jp Wed Jan 8 07:07:40 2003 From: JCA02266 @ nifty.ne.jp (Koji Arai) Date: Wed, 08 Jan 2003 07:07:40 +0900 (JST) Subject: [Lha-users] The Hacking of LHa for UNIX (1st draft) In-Reply-To: <20030106.002016.41659741.JCA02266@nifty.ne.jp> References: <20030105.130511.59492744.JCA02266@nifty.ne.jp> <20030106.002016.41659741.JCA02266@nifty.ne.jp> Message-ID: <20030108.070740.108740147.JCA02266@nifty.ne.jp> 新井です。 更新、現時点で支離滅裂です。解析の方針を立てても、脇道にそれ てドツボにはまってます。あまりにも頭の中が整理されてないので、 CVSの方はまだ更新してません。 # 自分の愚かさぶりを公表するだけですが。以下現時点の差分です。 Index: Hackinf_of_LHa =================================================================== RCS file: /cvsroot/lha/lha/Hackinf_of_LHa,v retrieving revision 1.3 diff -u -u -r1.3 Hackinf_of_LHa --- Hackinf_of_LHa 5 Jan 2003 21:56:41 -0000 1.3 +++ Hackinf_of_LHa 7 Jan 2003 21:47:26 -0000 @@ -1296,10 +1296,9 @@ static void match_insert() { - unsigned int off, h, max; + unsigned int off, h; unsigned int scan_end; - max = maxmatch; /* MAXMATCH; */ if (matchlen < THRESHOLD - 1) matchlen = THRESHOLD - 1; matchpos = pos; @@ -1315,10 +1314,9 @@ if (matchlen <= off + 2) { off = 0; - h = hval; scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; - search_dict(pos, hash[h], scan_end, off+2); + search_dict(pos, hash[hval], scan_end, off+2); } insert(); @@ -1367,7 +1365,7 @@ どうも、文字 text[pos-1] を出力しているだけのように思える。文字の出力 は、slide 辞書法では「辞書から見つからなかった場合」を意味するから、や はり最初の予想はあってそうなのだが・・・仕方ないので、output()の処理を -除いて見よう。これは、lh5, 6, 7 の場合、huf.c:output_st1(c, p) である。 +覗いて見よう。これは、lh5, 6, 7 の場合、huf.c:output_st1(c, p) である。 現時点で処理の内容を見てもわけがわからないが、結論から言うと第一引数 c は、文字で、第二引数 p は、位置である。冒頭の decode 処理で、文字 c は 長さも兼ねていると説明済みなので、(そして、text[pos-1] には現時点で文 @@ -1561,8 +1559,9 @@ / maxmatch{256} maxmatch{256} pos - <------------------------------> - remainder + <-------------------------------> + remainder + |------- 以前のデータ ---------|--- 新しいデータ ---------| ---------------------------------------------------------------------------- @@ -1596,6 +1595,334 @@ これで、一応は slide.c を網羅する事ができた。まだ不明な点は多いが、デ バッガで実際の処理を追いかければまたわかることがあるだろう。 + +・・・しばし、休息・・・ + +さて、デバッガでと以前は考えていたのだが、あきらめるのはまだ早い(元気 +が出たらしい)、そもそも最初に「デバッガを使わずにどこまで解読できるか」っ +と冒頭に書いてたのにたった2回の通読でもうあきらめようとしていた。が、 +ここまで書いてきた本書を何度か読み返したが、まだまだ検討の余地はある。 + +そこで、次からは matchlen に着目して見直しをはかることにした。matchlen +は出現頻度の高い変数でそのたびに予想を覆し混乱していたからだ。 + +encode() の概観を再掲載しよう。この中で、matchlen がどう変わって行くの +かを見て行く。 + +unsigned int +encode() +{ + int lastmatchlen; + unsigned int lastmatchoffset; + + /* (A) */ + init_slide(); + + /* (B) */ + remainder = fread_crc(&text[dicsiz], txtsiz-dicsiz, infile); + encoded_origsize = remainder; + matchlen = THRESHOLD - 1; + + pos = dicsiz; + + if (matchlen > remainder) matchlen = remainder; + + /* (C) */ + hval = ((((text[dicsiz] << 5) ^ text[dicsiz + 1]) << 5) + ^ text[dicsiz + 2]) & (unsigned)(HSHSIZ - 1); + + /* (D) */ + insert(); + + while (remainder > 0 && ! unpackable) { + /* (E) */ + lastmatchlen = matchlen; lastmatchoffset = pos - matchpos - 1; + --matchlen; + + /* (F) */ /* (G) */ + get_next(); match_insert(); + if (matchlen > remainder) matchlen = remainder; + + /* (H) */ + if (matchlen > lastmatchlen || lastmatchlen < THRESHOLD) { + /* (H.1) */ + encode_set.output(text[pos - 1], 0); + count++; + } else { + /* (H.2) */ + encode_set.output(lastmatchlen + (UCHAR_MAX + 1 - THRESHOLD), + (lastmatchoffset) & (dicsiz-1) ); + --lastmatchlen; + + while (--lastmatchlen > 0) { + get_next(); insert(); + count++; + } + get_next(); + matchlen = THRESHOLD - 1; + match_insert(); + if (matchlen > remainder) matchlen = remainder; + } + } +} + +ざっと、matchlen 絡みの動きを中心に見直してみたのだが別の事に気づいた。 +lastmatchlen, lastmatchoffset である。 + +これまでは off の位置や matchlen の状態に着目していたのだが、実際の書 +き出しを行っていたのは lastmatch〜 の方だった。つまり、off-1 の位置や +lastmatch〜 の状態がむしろ「現在」の状態で、off や matchlen は一歩ある +いは 2 歩先の状態だった。今度は off-1 の状態も意識の中に入れよう。 + +初期化(ループの前)は < 初期状態 > として書いた図を見て、さっさとループ +の中に入ろう。(E) だ。 + + /* (E) */ + lastmatchlen = matchlen; lastmatchoffset = pos - matchpos - 1; + --matchlen; + + /* (F) */ /* (G) */ + get_next(); match_insert(); + if (matchlen > remainder) matchlen = remainder; + +(F) まで処理が進んだ所をイメージして、図に起こそう。hval が pos の位置 +から 3 文字を示している事にも注意する。 + +---------------------------------------------------------------------------- +< ループ開始 > + + dicsiz=2^dicbit 2*2^dicbit + v v txtsiz + +-------------+-------------++------------+-------------+---+ + text | | || | | | + +-------------+-------------++------------+-------------+---+ + ^ / \ <---> + | pos-1 pos maxmatch{256} + matchpos + |----- lastmatchoffset -----| + <--> + lastmatchlen + + <------ remainder --------------> + + |--- この位置に最初の ---------| + データが読まれている + +< 各変数の値 > + + matchlen = 1 + matchpos = 0 + pos = dicsiz+1 + hval = pos の位置から3文字 + + lastmatchlen = 2 + lastmatchoffset = pos - matchpos - 1 (pos-1) +---------------------------------------------------------------------------- + +そして、match_insert() が呼ばれ、以下の条件で一致判定を行っていた。 + + /* (H) */ + if (matchlen > lastmatchlen || lastmatchlen < THRESHOLD) { + +じっくり考える。match_insert() では、もし見つかったら matchpos, +matchlen を更新し、見つからなかったら更新しなかった。(match_insert()の +検索対象は hval つまり、pos の位置から3文字だ) + +もし、見つからなかった場合(今は初期状態なので当然見つからない)、 + + matchlen{1} > lastmatchlen{2} + +の条件は偽だが、それよりもこの場合は、 + + lastmatchlen < THRESHOLD + +こちらの条件の方が重要なようだ。lastmatchlen の初期値は 2 (THRESHOLDは +3)なのだから match_insert() がどういう結果だろうと、(H) の条件は真だ。 +これは正しい処理(H が真であれば、見つからないの意味だった)。(H) の条件 +の右半分 lastmatchlen < THRESHOLD は、とりあえず初期状態のための条件だ +と言える。 + +では、仮に 以前の位置で見つかった場合というのを想定してみよう。仮に +matchpos を dicsiz の位置に置く、lastmatchlen は 5 とでもしておこう。 +(matchlen は、(E) から 4) + +---------------------------------------------------------------------------- +< ループ開始 > + + dicsiz=2^dicbit 2*2^dicbit + v v txtsiz + +-------------+-------------+-------------+-------------+---+ + text | | | | | | + +-------------+-------------+-------------+-------------+---+ + / \ + matchpos pos + + |--- ---| + lastmatchoffset + <-----> + lastmatchlen{5} + + <--- remainder ---> + + |--- この位置に最初の ---------| + データが読まれている + +< 各変数の値 > + + matchlen = 4 + matchpos = dicsiz + pos = 適当 + hval = pos の位置から3文字 + + lastmatchlen = 5 + lastmatchoffset = pos - matchpos - 1 (pos-1) +---------------------------------------------------------------------------- + +ここで、match_insert() した後、どうなるか? + + /* (H) */ + if (matchlen > lastmatchlen || lastmatchlen < THRESHOLD) { + +仮に見つからなかったとしよう。 + + matchlen{4} > lastmatchlen{5} + +は偽だ。 + + lastmatchlen{5} < THRESHOLD{3} + +でもあるから (H) の条件は偽だ???・・・そうだった。lastmatchlen に注 +目するのだった。(H.1) で、一つ前(pos-1)の文字を書くのだから、この条件 +判断は「真なら一つ前は一致していなかった」を表す。そして、以前は一致し +ていたから、match_insert() の結果にかかわらず(H) は偽にならなければお +かしい。 + +では、仮に match_insert() で見つかった場合を考えよう。matchlen = 10 に +なったとでもしよう。(H) は真になる。おかしいことが起こった。(H) の条件 +は「以前は一致していたが今回さらに長く一致した」ということを示している。 + +(H) の条件はなかなか難解だ。素直に条件式を読んでみると + + if (「一つ前の一致長よりも長く一致した」 || + 「一つ前は一致していなかった」) { + +そうなのだ。素直にこのとおりの条件なのだ。当初 (H) の意味を「見つかっ +たか、見つからなかったか?」という条件のはずだと先入観を持っていたがた +めに混乱していた。もう一つ、気づくのが遅すぎたが、match_insert() は、pos +の文字列が見つからない場合、matchlen を更新しないが (E) で、 + + lastmatchlen = matchlen; + --matchlen; + +という処理、つまり、matchlen < lastmatchlen の状態にあらかじめしている +ことから (H) の条件は、 + + if (「(match_insert()で文字列が見つかり) + 一つ前の一致長よりも長く一致した」 || + 「一つ前は一致していなかった」) { + +っと「(match_insert()で文字列が見つかり)」という条件が隠れている。以前、 +match_insert() で「見つかったかどうかの状態を関数の外に出力として出さ +ないのはなぜか?」っと思ったのだが、ここでは match_insert() の呼び出し +前に「matchlen をあらかじめ見つからなかった場合 に更新していた」の +だ。・・・むむむ。なかなか意外な処理だった。先入観を持ちすぎていたとい +うこともあるだろう。 + +まとめると (H) の条件(の左半分)はこうだ「たとえ、pos-1 の位置の文字列 +が辞書にあったとしても、pos の位置の文字列がより長く辞書と一致するなら +pos-1 の文字を平文で出力する」 + +もう一つここで、先入観があることに気づく。match_insert() は pos の位置 +の文字列を辞書から検索する処理だとここで考えていたが、そうではなかった。 +何やら先読みらしい事をしていたではないか。 + +もう一回、match_insert() の内容を確認し、(H) の正確な条件を考えよう。 + +改良版 match_insert() を再掲する。 + +static void match_insert() +{ + unsigned int off, h; + unsigned int scan_end; + + if (matchlen < THRESHOLD - 1) matchlen = THRESHOLD - 1; + matchpos = pos; + + off = 0; + for (h = hval; too_flag[h] && off < maxmatch - THRESHOLD; ) { + h = ((h << 5) ^ text[pos + (++off) + 2]) & (unsigned)(HSHSIZ - 1); + } + if (off == maxmatch - THRESHOLD) + off = 0; + + scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; + search_dict(pos, hash[h], scan_end, maxmatch); + + if (matchlen <= off + 2) { + off = 0; + + scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; + search_dict(pos, hash[hval], scan_end, off+2); + } + + insert(); +} + +最初の + + off = 0; + for (h = hval; too_flag[h] && off < maxmatch - THRESHOLD; ) { + h = ((h << 5) ^ text[pos + (++off) + 2]) & (unsigned)(HSHSIZ - 1); + } + if (off == maxmatch - THRESHOLD) + off = 0; + +この部分、まず、off は、0 から maxmatch - THRESHOLD - 1 (252) の範囲の +値を持つ。for の中のハッシュ関数から h は、pos+off の位置の 3 文字を表 +す。(ただし、例外がある。h が pos + maxmatch - THRESHOLD の文字列を表 +す場合、off は 0 だ。うむむこの意味はとりあえず無視しよう) + +そして、too_flag の内容によって、off の値が決まる。 + +too_flags[] はどのような場合に更新されるか slide.c 全体を確認してみる +と、まず初期状態ではすべての要素が 0 だ。そして、update() でも、すべて +の要素が 0 に初期化される。これらはともに初期状態であり、境界の状態で +もある。これ意外では search_dict() と名付けた関数で更新されるだけだ。 +この意味を確認しよう。search_dict() に該当する処理を再掲する。 + + while (scan_pos > scan_end) { + chain++; + + if (text[scan_pos + matchlen - off] == text[pos + matchlen]) { + ... 省略 ... + } + scan_pos = prev[scan_pos & (dicsiz - 1)]; + } + + if (chain >= LIMIT) + too_flag[h] = 1; + +LIMIT は、256 だ。256回ハッシュのリンクをたどっても辞書から文字列が見 +つからない場合に、too_flag[h] は 1 になる。つまり、h の文字が一度見つ +からなかったとわかると、そのことを too_flag[h] に記録しているどうやら +これは高速化(というよりは最悪の状態を避けるためのもの)の処理のようだ。 +次回、同じ轍を踏むのを避けているものと思われる。確認しようもう一度 +for ループの処理を見る + + off = 0; + for (h = hval; too_flag[h] && off < maxmatch - THRESHOLD; ) { + h = ((h << 5) ^ text[pos + (++off) + 2]) & (unsigned)(HSHSIZ - 1); + } + if (off == maxmatch - THRESHOLD) + off = 0; + +hval が見つからない事がわかっているなら off を進める。 + +# ん?insert() の処理で、hash[] を更新したとき too_flag[] を更新してい +# ないのはなぜだろう?前回match_insert() を呼び出して(too_flagを更新し +# てから)次の match_insert() までには当然 hash[] は更新される。 +# too_flag[hval]==1だとしても、一致文字列が見つからないとは限らないの +# では?うーん、またくじけそうだ。 で、どうも match_insert() の改造版じゃない方(改造版はoffに関 してまだ不完全)を見ると pos+off の位置の文字列の辞書上の位置 scan_pos を求め scan_pos - off したものと pos 位置の文字列を 比較しているので、結局比較対象は pos 位置の文字列。 match_insert() はどう転んでも pos の位置の文字列の辞書検索な のは間違いなさそう。(H) の条件はやっぱりあってるか? 今回の更新分は破棄しようかなあ。 -- 新井康司 (Koji Arai) From JCA02266 @ nifty.ne.jp Wed Jan 8 07:17:58 2003 From: JCA02266 @ nifty.ne.jp (Koji Arai) Date: Wed, 08 Jan 2003 07:17:58 +0900 (JST) Subject: [Lha-users] The Hacking of LHa for UNIX (1st draft) In-Reply-To: <20030106205247.54BE.TSUGIO@muc.biglobe.ne.jp> References: <20030105.130511.59492744.JCA02266@nifty.ne.jp> <20030106.002016.41659741.JCA02266@nifty.ne.jp> <20030106205247.54BE.TSUGIO@muc.biglobe.ne.jp> Message-ID: <20030108.071758.130241006.JCA02266@nifty.ne.jp> 新井です。 In message "Re: [Lha-users] The Hacking of LHa for UNIX (1st draft)" on Mon, 06 Jan 2003 21:01:33 +0900, Tsugio Okamoto wrote: > > 岡本です. > > なんか申し訳ないです.少しコメントします. そのように考える必要はないです。本人、楽しんでやってますので(^^ > offは検索を早くするための名残です.lhaを誕生させる試行錯誤した結果です. > もとは,こんなに汚いソースではなかったのです. off は難解です。まだわかりません。 > 辞書を検索してtext[pos-1]よりもtext[pos]から比較した方が長く > 一致するケースがあったため,2回検索して長く一致したら,先に > text[pos-1]で辞書に一致したとしてもそれを使わずに長いほうを > 選ぶということをしたらしいです. これも難解です。まだ苦し楽しんでます。 > > update() の残りを見る。 > > > > for (i = 0; i < HSHSIZ; i++) { > > j = hash[i]; > > hash[i] = (j > dicsiz) ? j - dicsiz : NIL; > > too_flag[i] = 0; > > } > > for (i = 0; i < dicsiz; i++) { > > j = prev[i]; > > prev[i] = (j > dicsiz) ? j - dicsiz : NIL; > > } > > > > 内容は、想像がつくので詳細は省略しよう。単に以前のデータが移動したので、 > > ハッシュ値を更新しているだけだ。しかし、これはなかなか無駄な処理だ。 > > > > 以前、text[] は環状バッファだろうと予想したが予想がはずれたことがわかっ > > た。環状バッファにしていれば、このハッシュの書き換えは不要にできただろ > > うと思うのだが・・・ > > # そのかわり、位置の大小比較が繁雑にならないので、これはこれで良いのか > > # もしれない。どちらが優れているかは実験してみなければわからないだろう。 > > これは辞書から消える文字列に該当するハッシュ値をハッシュリスト > から消すためと思いました. > 無くても大丈夫ですか? まあ、ちょっと見て思っただけですから。基本的に私の考えはあさ はかです(^^;。予測ばかりでちゃんと実験もしてない私の言葉を鵜 のみにする必要はないというか、大丈夫か?っと聞かれるとオロオ ロするというか(汗;) -- 新井康司 (Koji Arai) From tsugio @ muc.biglobe.ne.jp Thu Jan 9 00:59:13 2003 From: tsugio @ muc.biglobe.ne.jp (Tsugio Okamoto) Date: Thu, 09 Jan 2003 00:59:13 +0900 Subject: [Lha-users] The Hacking of LHa for UNIX (1st draft) In-Reply-To: <20030108.071758.130241006.JCA02266@nifty.ne.jp> References: <20030106205247.54BE.TSUGIO@muc.biglobe.ne.jp> <20030108.071758.130241006.JCA02266@nifty.ne.jp> Message-ID: <20030109003659.54CA.TSUGIO@muc.biglobe.ne.jp> 岡本です. On Wed, 08 Jan 2003 07:17:58 +0900 (JST) Koji Arai wrote: > 新井です。 > > > offは検索を早くするための名残です.lhaを誕生させる試行錯誤した結果です. > > もとは,こんなに汚いソースではなかったのです. > > off は難解です。まだわかりません。 うる覚えなのですが,もともとはこんなアイディアからです. 現在の圧縮しようとしている文字列の位置: pos として, text[pos],text[pos+1],text[pos+2]をハッシュ値として,ハッシュリスト から辞書内の位置を示す値を取得します.まだ本当に一致するのか, 一致してもどのくらいの長さで一致するのかは不明です. 次に,同じハッシュ値のハッシュリストをだどっていくと,辞書内に 3文字以上一致する可能性のある文字列の位置の候補がいくつか検索 できるわけですが,その候補の位置sposからはじまる文字列と, 現在の位置posからはじまる文字列を比較します. この比較した結果がmatchposとかmatchlenとかになります. で,候補になる位置からはじまる文字列を次々に比較していきます. 基本的に最長一致する位置の辞書を使うことになります. これが通常ですが,ある特定の3文字のハッシュ値を使うと リストが長くなる可能性があります. これを,現在の位置posから少し右にずらして例えば4くらいにし, そこのtext[pos+4],text[pos+5],text[pos+6]をハッシュ値として, ハッシュリストを検索していきます. ただし,候補の文字列の比較は,text[pos],text[pos+1],...から始めます. こうすると,3+4文字以内となる文字長の検索はできなくなってしまうの ですが,先頭のtext[pos]…をハッシュ値として検索するよりも, text[pos+4]…をハッシュ値として検索した方がリストが短く,検索時間が 短くなる可能性があります. あ,でも,今は長いリストを持つハッシュ値だけこうしていたのかな? だんだん忘れかけているのですが,基本的な使い道はこういうことだったと 思います. ----------------------------------------------------- Tsugio Okamoto (E-Mail:tsugio @ muc.biglobe.ne.jp) From JCA02266 @ nifty.ne.jp Mon Jan 13 03:51:49 2003 From: JCA02266 @ nifty.ne.jp (Koji Arai) Date: Mon, 13 Jan 2003 03:51:49 +0900 (JST) Subject: [Lha-users] The Hacking of LHa for UNIX (1st draft) In-Reply-To: <20030109003659.54CA.TSUGIO@muc.biglobe.ne.jp> References: <20030106205247.54BE.TSUGIO@muc.biglobe.ne.jp> <20030108.071758.130241006.JCA02266@nifty.ne.jp> <20030109003659.54CA.TSUGIO@muc.biglobe.ne.jp> Message-ID: <20030113.035149.48030036.JCA02266@nifty.ne.jp> 新井です。 In message "Re: [Lha-users] The Hacking of LHa for UNIX (1st draft)" on Thu, 09 Jan 2003 00:59:13 +0900, Tsugio Okamoto wrote: > > 岡本です. > > On Wed, 08 Jan 2003 07:17:58 +0900 (JST) > Koji Arai wrote: > > > 新井です。 > > > > > offは検索を早くするための名残です.lhaを誕生させる試行錯誤した結果です. > > > もとは,こんなに汚いソースではなかったのです. > > > > off は難解です。まだわかりません。 > > うる覚えなのですが,もともとはこんなアイディアからです. 納得しました。Hacking_of_LHa を更新しました。 Index: Hackinf_of_LHa =================================================================== RCS file: /cvsroot/lha/lha/Hackinf_of_LHa,v retrieving revision 1.3 retrieving revision 1.4 diff -u -u -r1.3 -r1.4 --- Hackinf_of_LHa 5 Jan 2003 21:56:41 -0000 1.3 +++ Hackinf_of_LHa 12 Jan 2003 18:46:22 -0000 1.4 @@ -1,4 +1,4 @@ -$Id: Hackinf_of_LHa,v 1.3 2003/01/05 21:56:41 arai Exp $ +$Id: Hackinf_of_LHa,v 1.4 2003/01/12 18:46:22 arai Exp $ The Hacking of LHa for UNIX (1st draft) ------------------------------------------- @@ -1296,10 +1296,9 @@ static void match_insert() { - unsigned int off, h, max; + unsigned int off, h; unsigned int scan_end; - max = maxmatch; /* MAXMATCH; */ if (matchlen < THRESHOLD - 1) matchlen = THRESHOLD - 1; matchpos = pos; @@ -1313,12 +1312,11 @@ scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; search_dict(pos, hash[h], scan_end, maxmatch); - if (matchlen <= off + 2) { + if (off > 0 && matchlen <= off + 2) { off = 0; - h = hval; scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; - search_dict(pos, hash[h], scan_end, off+2); + search_dict(pos, hash[hval], scan_end, off+2); } insert(); @@ -1367,7 +1365,7 @@ どうも、文字 text[pos-1] を出力しているだけのように思える。文字の出力 は、slide 辞書法では「辞書から見つからなかった場合」を意味するから、や はり最初の予想はあってそうなのだが・・・仕方ないので、output()の処理を -除いて見よう。これは、lh5, 6, 7 の場合、huf.c:output_st1(c, p) である。 +覗いて見よう。これは、lh5, 6, 7 の場合、huf.c:output_st1(c, p) である。 現時点で処理の内容を見てもわけがわからないが、結論から言うと第一引数 c は、文字で、第二引数 p は、位置である。冒頭の decode 処理で、文字 c は 長さも兼ねていると説明済みなので、(そして、text[pos-1] には現時点で文 @@ -1561,8 +1559,9 @@ / maxmatch{256} maxmatch{256} pos - <------------------------------> - remainder + <-------------------------------> + remainder + |------- 以前のデータ ---------|--- 新しいデータ ---------| ---------------------------------------------------------------------------- @@ -1597,7 +1596,261 @@ これで、一応は slide.c を網羅する事ができた。まだ不明な点は多いが、デ バッガで実際の処理を追いかければまたわかることがあるだろう。 - +・・・しばし、休息・・・ + +さて、デバッガでと以前は考えていたのだが、あきらめるのはまだ早い(元気 +が出たらしい)、そもそも最初に「デバッガを使わずにどこまで解読できるか」っ +と冒頭に書いてたのにたった2回の通読でもうあきらめようとしていた。が、 +ここまで書いてきた本書を何度か読み返したが、まだまだ検討の余地はある。 + +まず、match_insert() の処理でわからなかった部分を解読しよう。実は、こ +れに関してはどうしてもわからず悩んでいたところ、Lha for UNIX のメンテ +ナである岡本さんに教えてもらうことができた(ありがとうございます)その内 +容を確認しつつ match_insert() を見ることにする。 + +まずは、復習だ。通常の状態に関しては match_insert() の解読は済んでいる。 +match_insert() は、text[pos] から始まる文字列を辞書から検索し、見つかっ +た位置と一致長を matchpos, matchlen に設定する処理だ。そして、ついでに +insert() で、text[pos] の位置をハッシュ配列に記録し、次回の検索に備え +れこともしている。 + +では、不明な部分はなんだったかというと too_flag[] まわりである。 +too_flag のフラグが立っていると。辞書検索の頼りとなるハッシュ値を変更 +している。この部分がまったく皆目検討がつかなかったのだ。これに関してソー +スを読み進めよう。以下ソースを再掲する。 + +static void match_insert() +{ + unsigned int scan_pos, scan_end, len; + unsigned char *a, *b; + unsigned int chain, off, h, max; + + max = maxmatch; /* MAXMATCH; */ + if (matchlen < THRESHOLD - 1) matchlen = THRESHOLD - 1; + matchpos = pos; + + off = 0; + for (h = hval; too_flag[h] && off < maxmatch - THRESHOLD; ) { + h = ((h << 5) ^ text[pos + (++off) + 2]) & (unsigned)(HSHSIZ - 1); + } + if (off == maxmatch - THRESHOLD) off = 0; + for (;;) { + chain = 0; + scan_pos = hash[h]; + scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; + while (scan_pos > scan_end) { + chain++; + + if (text[scan_pos + matchlen - off] == text[pos + matchlen]) { + { + a = text + scan_pos - off; b = text + pos; + for (len = 0; len < max && *a++ == *b++; len++); + } + + if (len > matchlen) { + matchpos = scan_pos - off; + if ((matchlen = len) == max) { + break; + } + } + } + scan_pos = prev[scan_pos & (dicsiz - 1)]; + } + + if (chain >= LIMIT) + too_flag[h] = 1; + + if (matchlen > off + 2 || off == 0) + break; + max = off + 2; + off = 0; + h = hval; + } + prev[pos & (dicsiz - 1)] = hash[hval]; + hash[hval] = pos; +} + +まず、too_flag[] は、最初すべての要素が 0 である。そして、新たにファイ +ルを読むとき(update())も 0 に再初期化されるのだった。このフラグが立つ +条件はというと、この match_insert() の中だけである。その処理は + + if (chain >= LIMIT) + too_flag[h] = 1; + +この部分だけだ。chain が LIMIT以上になったら h (これは検索対象のハッシュ +値だ)に関して、フラグを立てる。chain は while ループ(これは文字列の照 +合を行う処理)のループ回数だ。h に関しての検索が LIMIT{256} 以上の場合 +に too_flag[h] のフラグが立っている。 + +while ループは一致文字列の一致長が最長一致長に達するか、辞書を最後まで +探索するまでループする。つまり、あるハッシュ h に関してそのチェーンが +256 以上のものに関しては、too_flag[h] が 1 になっている。 + +では、そのような h に関して、match_insert() がどのような処理になってい +るかを見る。まず初期化部分 + + max = maxmatch; /* MAXMATCH; */ + if (matchlen < THRESHOLD - 1) matchlen = THRESHOLD - 1; + matchpos = pos; + +これは、とりあえず無視。出力となる matchpos は pos で初期化されること +には注意しておこう(前回は気づかなかった事だ)。 + + off = 0; + for (h = hval; too_flag[h] && off < maxmatch - THRESHOLD; ) { + h = ((h << 5) ^ text[pos + (++off) + 2]) & (unsigned)(HSHSIZ - 1); + } + if (off == maxmatch - THRESHOLD) off = 0; + +通常 off は、0 なのだが、too_flag[h] が 1 であるものに関しては値が変わ +る。検索対象となる文字列 text[pos](のハッシュ値) hval に関して、 +too_flag[h] が立っていれば、(このハッシュのチェーンが 256 以上であるこ +とが事前にわかっていれば) + + h = ((h << 5) ^ text[pos + (++off) + 2]) & (unsigned)(HSHSIZ - 1); + +で、検索対象となるハッシュ値を変更している。このハッシュ値が示すのは元 +の検索対象文字列の 1 文字先だ。 + +---------------------------------------------------------------------------- + + |--- c --| + |--- b --| | + |-- a ---| | | + +-------------+--------+--------+ +text | | | | | | | | + +-------------+--------+--------+ + \ \ + pos pos+1(off=1) + +---------------------------------------------------------------------------- + +元の検索対象文字列が図の a だとすると、これを図の b にしている。初期化 +部のループは、もしこの b のハッシュチェーンに関して too_flag[h] がさら +に 1 であるならさらに 先の文字列をハッシュ値とするようになっている。 +(これは元の pos の 2 文字先を示す。図の c の部分だ) h は、pos+off から +の3文字のハッシュ値を示すものと言う事だ。 + +ただし、h があまりにも先の方を見るようなハメになれば(off が maxmatch - +THRESHOLD) off は 0 に再設定されるが、このとき h はそのままだ。この意 +味はまだわからないが、バグなのではないかと想像している(h = hval に再設 +定する必要があるだろう) + +では off = 1 だとして本処理を見ることにしよう。外側の for ループに関し +ては、while ループを2回実行するかどうかだけのものだった。なので、 +while ループ部だけを見てみよう。 + + chain = 0; + scan_pos = hash[h]; + scan_end = (pos > dicsiz) ? pos + off - dicsiz : off; + while (scan_pos > scan_end) { + chain++; + + if (text[scan_pos + matchlen - off] == text[pos + matchlen]) { + { + a = text + scan_pos - off; b = text + pos; + for (len = 0; len < max && *a++ == *b++; len++); + } + + if (len > matchlen) { + matchpos = scan_pos - off; + if ((matchlen = len) == max) { + break; + } + } + } + scan_pos = prev[scan_pos & (dicsiz - 1)]; + } + + if (chain >= LIMIT) + too_flag[h] = 1; + +scan_pos, scan_end に関しては検索開始位置と終了位置と言う事でもう良い +だろう。で、最初の if の条件に着目する。 + + if (text[scan_pos + matchlen - off] == text[pos + matchlen]) { + +これが真となる状態を図示しよう。 + +---------------------------------------------------------------------------- + + |-- c ---| + |-- a ---| |--- b --| + +---------------+--------+--------------------+--------+--------+ +text | | |x'| | | | |x | | | | + +---------------+--------+--------------------+--------+--------+ + ^ \ \ + scan_pos pos pos+1(off=1) + +---------------------------------------------------------------------------- + +まず、if 条件の左辺 + + text[scan_pos + matchlen - off] + +matchlen は、match_insert() に入る直前に 2 に初期化されている(最初は) +ので、照合するのは図の x' だ。 + +if 条件の右辺 + + text[pos + matchlen] + +これは、図の x の位置だ。x' == x ならば本格的に照合を開始する。 + + { + a = text + scan_pos - off; b = text + pos; + for (len = 0; len < max && *a++ == *b++; len++); + } + +ここで比較しているのは、図の a と b だ。b は、off がどのような場合でも +変わらないが、a は、off が大きければ大きい程左側を指す。off が例えば +3 であるときの場合も見てみよう。 + +---------------------------------------------------------------------------- + + |-- a ---| |--- b --|-- c ---| + +---------------+--------+--------------------+--------+--------+ +text | x'| | | | | | |x | | | | + +---------------+--------+--------------------+--------+--------+ + ^ \ \ + scan_pos pos pos+3(off=3) + +---------------------------------------------------------------------------- + +結局比較しているのは、pos からの文字列のハッシュ値を求めた場合と何も変 +わらない。off でいくら先を見ようとも比較するのは pos の位置だ。なぜこ +のようなことをするのだろうか?これは最初どうしてもわからなかったのだが、 +説明を受けて納得した。 + +これは単に効率(速度)のためということだ。もし、図の b に関して照合文字 +列の候補があまりにも多い場合(too_flag[h]=1)、ハッシュのチェーンを何度 +もたどる事になるので効率が悪い。しかし、辞書検索のキーを何文字か進める +事で、この可能性を減らす事ができる。少なくとも最悪の 256 よりはマシに +なるようなものを選んでいる。そうして、この while ループのループ回数を +減らしているのだ。どうせ探したいのは最長一致文字列なので先の方の文字列 +が一致しないと話にならないのだからこれは合理的だ。 + +これで、外側の for ループも納得だ。これは while ループをある条件でやり +直す処理だった。 + + if (matchlen > off + 2 || off == 0) + break; + +最長一致長が見つかるか、あるいは off が 0 であればさっさとこの処理は終 +るのだが、もし off を進めて照合を行っていたとして、最長一致文字列が見 +つからなかったとすると + + max = off + 2; + off = 0; + h = hval; + +という条件で照合をやり直す。これは元の文字列で照合をやり直すということ +だからつまりは、最悪のハッシュチェーンを仕方ないから辿り直そうと言う事 +だ。 + +細部は未だ目をつぶっているのだがこれで match_insert() の解読は終りだ。 + + # Local Variables: # mode : indented-text # indent-tabs-mode: nil ありがとうございました。m(__)m -- 新井康司 (Koji Arai) From JCA02266 @ nifty.ne.jp Mon Jan 13 08:13:08 2003 From: JCA02266 @ nifty.ne.jp (Koji Arai) Date: Mon, 13 Jan 2003 08:13:08 +0900 (JST) Subject: [Lha-users] The Hacking of LHa for UNIX (1st draft) In-Reply-To: <20030113.035149.48030036.JCA02266@nifty.ne.jp> References: <20030108.071758.130241006.JCA02266@nifty.ne.jp> <20030109003659.54CA.TSUGIO@muc.biglobe.ne.jp> <20030113.035149.48030036.JCA02266@nifty.ne.jp> Message-ID: <20030113.081308.127323558.JCA02266@nifty.ne.jp> 新井です。 更新。これで一通りの解読が完了。全体を見直して体裁を整えた後は huf.c に移るか、slide.c のソース書き換えを行うか? Index: Hackinf_of_LHa =================================================================== RCS file: /cvsroot/lha/lha/Hackinf_of_LHa,v retrieving revision 1.4 retrieving revision 1.5 diff -u -u -r1.4 -r1.5 --- Hackinf_of_LHa 12 Jan 2003 18:46:22 -0000 1.4 +++ Hackinf_of_LHa 12 Jan 2003 23:07:31 -0000 1.5 @@ -1,4 +1,4 @@ -$Id: Hackinf_of_LHa,v 1.4 2003/01/12 18:46:22 arai Exp $ +$Id: Hackinf_of_LHa,v 1.5 2003/01/12 23:07:31 arai Exp $ The Hacking of LHa for UNIX (1st draft) ------------------------------------------- @@ -1850,6 +1850,142 @@ 細部は未だ目をつぶっているのだがこれで match_insert() の解読は終りだ。 +match_insert() の処理とは以下の通りだ。 + +---------------------------------------------------------------------------- + match_insert() は、text[pos] から始まる文字列に一致する文字列を辞書 + から検索し、見つかった位置と一致長を matchpos, matchlen に設定する。 + + もし、最長一致文字列が見つからなければ matchpos は、off に設定され、 + matchlen は更新されない。 + + 見つかった場合、matchlen は呼び出し前の matchlen よりも大きくなる。 + (呼び出し前では matchlen の意味は最低限一致しなくてはならない文字列 + 長で、照合条件の一つになっている) + + この関数の入力は + + matchlen + pos + + 出力は + + matchlen + matchpos + + といったところ。 + + さらに、insert() と同様の処理で、pos の位置をハッシュ配列に記録し、 + 次回の検索に備える。これはついでの処理。 +---------------------------------------------------------------------------- + +これを踏まえた上で処理内容を再読しよう。(E) 〜 (H) だ。 + + /* (E) */ + lastmatchlen = matchlen; lastmatchoffset = pos - matchpos - 1; + --matchlen; + + /* (F) */ /* (G) */ + get_next(); match_insert(); + if (matchlen > remainder) matchlen = remainder; + + /* (H) */ + if (matchlen > lastmatchlen || lastmatchlen < THRESHOLD) { + /* (H.1) */ + encode_set.output(text[pos - 1], 0); + count++; + } else { + +(H) の条件とは何なのかを見る。この条件が真なら、文字列をそのまま出力す +るのだが、素直に slide 辞書法の処理を考えればこの条件は「辞書から見つ +からなかった場合」となる。が、実際にはもう少し複雑だ。 + + /* (H) */ + if (matchlen > lastmatchlen || lastmatchlen < THRESHOLD) { + +matchlen は、pos の位置の文字列が見つかった辞書上の長さ +lastmatchlen は、pos-1 の位置の文字列が見つかった辞書上の長さ + +であるとすると、この条件は、「pos の位置で見つかった長さが、pos-1 の位 +置で見つかった長さよりも長ければ」っとなる。 + +これはつまり、pos-1 と pos のニ箇所に関して辞書を検索して、より長くマッ +チする方を選ぼうとしているわけだ。matchlen の方が長いなら 1 つ前 +(pos-1)の文字はそのまま出力し、次のループに移る(もし、次の文字が +さらに長くマッチするなら。またそのまま出力される) + +この条件で「見つからなかった場合」というのはどう表されているかを考える。 +もし、pos の文字列が辞書になければ pos - 1 の文字列は、どうすべきかと +いうと「pos-1 の文字列が見つかってなければ。そのまま出力。辞書にあった +なら のペアを出力」っとならなければな +らない。 + +lastmatchlen は、初期状態では THRESHOLD - 1 であったので、見つからなかっ +たという条件は (H) の右側の条件 lastmatchlen < THRESHOLD でまず表され +ている。 + +では、例えば lastmatchlen が 5 であったとしよう。このとき (E) の処理で +matchlen は lastmatchlen - 1 つまり、4 に設定される。そして、match_insert() +で次の文字列がもし辞書から見つからなければ matchlen は更新されないので + matchlen < lastmatchlen +となる。このような条件(前回見つかり、今回見つからない)場合に限り、(H.2) +の処理が実行されるようになっている。では、(H.2) の処理を追いかけよう。 + +まず、この状態を図示する。 + +---------------------------------------------------------------------------- + + lastmatchlen lastmatchlen + |-- --| |-- --| + +---------------+--------------+--------------+--------------+--+ +text | | | | | | | | | | | | | | + +---------------+--------------+--------------+--------------+--+ + ^ | \ \ + matchpos pos-1 pos pos2 + + |--------------------------| + lastmatchoffset + +---------------------------------------------------------------------------- + + + /* (H.2) */ + encode_set.output(lastmatchlen + (UCHAR_MAX + 1 - THRESHOLD), + (lastmatchoffset) & (dicsiz-1) ); + --lastmatchlen; + + while (--lastmatchlen > 0) { + get_next(); insert(); + count++; + } + get_next(); + matchlen = THRESHOLD - 1; + match_insert(); + if (matchlen > remainder) matchlen = remainder; + } + +まず、<長さ, 位置> のペアを出力する。これはいいだろう。出力する「位置」 +は0 なら 1 文字前を表すので、実際のオフセット pos - 1 - matchpos より +も 1 小さい値になっていることに注意しておこう。 + +そして、lastmatchlen は 1 引かれる。この場合例えば 4 になる。したがっ +て、次のループでは 3 文字 pos が先送りされる(4 ではない)。pos は既に 1 +文字先に進んでいるので、最初に 1 引くのはこのことを考慮している。while +ループが終った後はpos の位置は実際に出力した文字列の最後の文字 pos2-1 +を指していることになる。 + +そして、get_next() でまた 1 文字先に送る。pos は図の pos2 の位置になる。 +そして、match_insert() で、この位置の文字列を照合する。matchlen は、 +THRESHOLD - 1 に初期化されるので pos2 の位置の文字列が辞書から見つから +なければ matchlen は、THRESHOLD-1 だ。これは初期状態と同じ状態を示すの +で、次のループの処理も想像がつく((H) の条件の右側 lastmatchlen < THRESHOLD +が有効になる)。では、見つかった場合はというと、次のループでさらに先 +pos2+1 の照合結果と比較されるのでこの処理も想像がつく。 + +最初、どうにもこの処理内容が理解できなかったのだが「現在の文字列と、次 +の文字列のそれぞれで辞書を検索し、より長く見つかった方を使う」という最 +適化を行っている事がわかってしまった後は解読は簡単だった。(実はこの事 +実も教えてもらった事だ。全然ダメじゃん) # Local Variables: # mode : indented-text From JCA02266 @ nifty.ne.jp Sun Jan 19 17:38:15 2003 From: JCA02266 @ nifty.ne.jp (Koji Arai) Date: Sun, 19 Jan 2003 17:38:15 +0900 (JST) Subject: [Lha-users] The Hacking of LHa for UNIX (2nd draft) In-Reply-To: <20030113.081308.127323558.JCA02266@nifty.ne.jp> References: <20030109003659.54CA.TSUGIO@muc.biglobe.ne.jp> <20030113.035149.48030036.JCA02266@nifty.ne.jp> <20030113.081308.127323558.JCA02266@nifty.ne.jp> Message-ID: <20030119.173815.68202077.JCA02266@nifty.ne.jp> 新井です。 更新しました。 > 更新。これで一通りの解読が完了。全体を見直して体裁を整えた後は huf.c > に移るか、slide.c のソース書き換えを行うか? huf.c に指しかかる事にしました(というわけで、2nd draft)。 slide.c の書き換えは最新の CVS に反映しました。処理内容がわ かりやすくなったかどうかは定かではないですが(^^; Index: Hackinf_of_LHa =================================================================== RCS file: /cvsroot/lha/lha/Hackinf_of_LHa,v retrieving revision 1.5 retrieving revision 1.7 diff -u -u -r1.5 -r1.7 --- Hackinf_of_LHa 12 Jan 2003 23:07:31 -0000 1.5 +++ Hackinf_of_LHa 19 Jan 2003 08:28:31 -0000 1.7 @@ -1,6 +1,6 @@ -$Id: Hackinf_of_LHa,v 1.5 2003/01/12 23:07:31 arai Exp $ +$Id: Hackinf_of_LHa,v 1.7 2003/01/19 08:28:31 arai Exp $ - The Hacking of LHa for UNIX (1st draft) + The Hacking of LHa for UNIX (2nd draft) ------------------------------------------- Koji Arai @@ -12,15 +12,6 @@ みただけのものだ。(休みが明けるとまた忙しくなるので、これ以上まったく 何もしないかもしれない) -現時点では、slide.c の解析だけである。huf.c も同時進行で解析中だが、文 -体が異なる(読みものとしての体裁を整えていない)ので公開するとしても別文 -書になるだろう(huf.c の解析文書は現時点でデバッガによるおっかけが中心 -のメモでしかない) - -# 本当は、huf.c の解析を先に行っていた slide 辞書の encoding 部はなく -# ても LHA 互換アーカイバは作れそうだったからだが、huf.c は slide.c -# に比べて難解な部分が多そうだから、後回しにした。 - 本書は、まだ未完成である。にもかかわらず公開するのはこれ以上続かないか もしれないからである(気が向いたらまた続きを書くだろう。あるいは応援の お手紙がくればやる気が出るかもしれない)。 @@ -35,11 +26,33 @@ =============================================================================== o 表記について -* 関数は、その定義ソースfile.c と関数名 func() を示すのに +* 関数は、その定義ソース file.c と関数名 func() を示すのに file.c:func() という記述を使う -* +* 配列の添字は、Python言語のスライス演算子の記法に準じた + + a[m:n] は、m <= i < m+n の範囲の a[i] を意味する。 + +* 値の範囲は、Ruby言語の範囲演算子の記法に準じた。これを配列の + 添字に使用する場合もある。 + + m <= i <= n -> i = m..n + m <= i < n -> i = m...n + + a[m..n] は、m <= i <= n の範囲の a[i] を意味する。 + +* m の n 乗 は、m^n で表す。^ は、排他的論理和としても利用されるが + 文脈から判断してもらう。 + +* v{n} は、変数 v の値が n であることを表す。n は、サンプルの値であったり + 定数の値であったりする。 + + v=n は代入文 + + 配列の内容は、 + ary[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0 } + のように書く o 用語について @@ -49,8 +62,19 @@ * 復号 復号化、復号語、復号文 +* フレーズ + +* slide 辞書法 + +* Huffman 法 + 動的 Huffman 法、静的 Huffman 法 + =============================================================================== + +slide 辞書法 (slide.c) +---------------------- + まず、構造について考える。 slide辞書法は、encoding にさまざまな工夫が凝らされるのでとても複雑だが、 @@ -101,7 +125,7 @@ のようになるはず。ここで、tokensiz は token の最大サイズで、最長一致長 を表す。この値が大きければ大きい程、圧縮効率は良くなるはずで、lha では、 これは MAXMATCH{256}である。また、dict は辞書でこのサイズは lha の --lz5- メソッドでは、8192 となっている。この辞書も大きければ大きい程良 +-lh5- メソッドでは、8192 となっている。この辞書も大きければ大きい程良 いはずだ。その方が一致文字列が見つかりやすい。(ただし、辞書が大きいと 一致位置を示す情報 の情報量が増えるはずだし、速度も遅くなる だろう。後で検証する) @@ -714,7 +738,7 @@ } remainder を消費し、pos を進めている。予想通りだ。ひとまず if の条件は -無視すると直後で hash 値を求め直している。このハッシュ関数は、以前のハッ +無視すると、直後で hash 値を求め直している。このハッシュ関数は、以前のハッ シュ値を利用しているが、これは pos が以前より + 1 されていることを考え ると関連が見えて来る。以前のhash関数を pos の関数として書き直すと @@ -1167,7 +1191,8 @@ } -offは 0 なので、text[scan_pos + matchlen] != text[pos + matchlen] という条件の場合を想定するわけだが、 +offは 0 なので、text[scan_pos + matchlen] != text[pos + matchlen] とい +う条件の場合を想定するわけだが、 text[scan_pos + matchlen] @@ -1244,17 +1269,12 @@ } この処理では a, b といういかにも局所な名前の変数が使われている。これは、 -本当にこのブロック内にで局所的なもののようだ。ならば定義位置もこのブロッ +本当にこのブロック内で局所的なもののようだ。ならば定義位置もこのブロッ ク内にして本当に局所的にして欲しかった。 さらに、この処理は単に文字列 a, b を比較しているだけのようだ。memcmp() ではまずいのかと言うとここで求めているものが「どこまで一致したか(len)」 -のようなので、memcmp() では役不足だ。仕方ないので関数をでっちあげて抽 -象化をはかろう。memcmp_ret_len()(我ながら変な名前だ)という関数があった -とするとこの部分は - len = memcmp_ret_len(&text[scan_pos-off], &text[pos], max); - -っとなる。返り値は一致した文字列長だ。 +のようなので、memcmp() では役不足だ。 その次の処理、 @@ -1330,10 +1350,11 @@ 更新するは余計に思う)なのだろうが、最長一致文字列が見つからなかったと いうのはどう判断されるのだろう? -まず、search_dict() で見つからなかった場合、matchlen, matchpos は更新 -されない。そして、おそらく 2 度目の search_dict() の呼び出しが行われる。 -が、too_flag[] というので、判断できそうな気もするがこれはむしろハッシュ -のチェーンをたどりすぎるのを止めるためのフラグであるように思える。 +まず、search_dict() で見つからなかった場合、matchlen は更新されない +(matchpos は、pos になる)。そして、おそらく 2 度目の search_dict() の +呼び出しが行われる。が、too_flag[] というので、判断できそうな気もする +がこれはむしろハッシュのチェーンをたどりすぎるのを止めるためのフラグで +あるように思える。 2度目の search_dict()で、max の値が変わるのが鍵だろうか?。今回の場合、 max は 256 から 2 になる。最長一致長として 2 が限界値になると、 @@ -1384,6 +1405,8 @@ の変数が大勢に影響を与える事はないだろうからこれ以上は見ないと言うのも アリだ。 +# その後、dhuf.c:decode_p_dyn() でのみ count を使用している事がわかった。 + 次は (H.2) である。これがまた難解なのだがゆっくり片付けよう。 } else { @@ -1409,8 +1432,8 @@ 位置 lastmatchoffset & (dicsiz-1) となっている。冒頭の decode() の解析で、長さは 253 を足す事は確認済み -だ(ふと -lhs- の場合 254 を足すという動作が、encoding 部分では考慮され -ていないようだと気づく。いいのかそれで?)。ところで、一致長 +だ(-lhs- の場合 254 を足すという動作が、encoding 部分では考慮され +てないのは、-lhs- の encoding 機能がないからだ)。ところで、一致長 lastmatchlen は 3 以上で初めて 255 を越えることができる。以前予想した、 THRESHOLD の意味「最低限一致しなければならない長さ」はあっているらしい。 @@ -1456,6 +1479,10 @@ match_insert() が行われる。どうやら pos は次のループからは、 2 文字文 先に進んでしまうようだ。なぜだろう? +# 後でわかった事だが、while (--lastmatchlen > 0) のループ回数を読み間 +# 違えた。例えば、lastmatchlen が 1 なら、この while ループ内では +# get_next() は1回も呼ばれない。 + どうにもソースを見ただけで解読するには、このあたりが限界のようだ。どう しても細部がわからないし、事実が見えないから予想の積み重ねがたまって不 安になる。 @@ -1466,7 +1493,7 @@ きをデバッガで追うことで、これまでの解析結果を検証してみよう。 ・・・っと、その前に、ここまでですべての関数を網羅してしまったと思って -たのだが、一つ忘れていたものがあった。update() だ。この関数は、 +いたのだが、一つ忘れていたものがあった。update() だ。この関数は、 get_next() で呼び出されていたのだが、以前は無視していた。先にここを見 ておこう。 @@ -1572,6 +1599,10 @@ らしい処理は、match_insert() の冒頭にあった(が、現時点で詳細には触れて いない)。 +# maxmatch 分の余分な領域は、pos の位置から最大 maxmatch 長の文字列照 +# 合を行うために必要な領域。先読みとはまた見当外れなことを書いたものだ。 +# ちょっと考えればわかることなのに・・ + update() の残りを見る。 for (i = 0; i < HSHSIZ; i++) { @@ -1612,7 +1643,7 @@ match_insert() は、text[pos] から始まる文字列を辞書から検索し、見つかっ た位置と一致長を matchpos, matchlen に設定する処理だ。そして、ついでに insert() で、text[pos] の位置をハッシュ配列に記録し、次回の検索に備え -れこともしている。 +ることもしている。 では、不明な部分はなんだったかというと too_flag[] まわりである。 too_flag のフラグが立っていると。辞書検索の頼りとなるハッシュ値を変更 @@ -1846,18 +1877,83 @@ という条件で照合をやり直す。これは元の文字列で照合をやり直すということ だからつまりは、最悪のハッシュチェーンを仕方ないから辿り直そうと言う事 -だ。 +だ。さらに、pos から pos+off+3 までの文字列が、辞書から見つからなかっ +たので、最大一致長を off + 2 として条件を緩めている(なぜこれが条件を緩 +める事になるかと言うと while ループは最大一致長の文字列が見つかったら +すぐに抜けるからだ)。 + +ところで、match_insert() の先の処理は以下の書き換えを行うともう少し見 +やすくなる。(と思う)。 + +o scan_beg という変数を用意し、これを scan_pos - off にする。 +o scan_end は、pos - dicsiz にする。 +o while 条件を while (scan_pos != NIL && scan_beg > scan_end) にする。 + +以下 + + unsigned int scan_pos = hash[h]; + int scan_beg = scan_pos - off; + int scan_end = pos - dicsiz; + + chain = 0; + while (scan_pos != NIL && scan_beg > scan_end) { + chain++; + + if (text[scan_beg + matchlen] == text[pos + matchlen]) { + { + unsigned char *a = &text[scan_beg]; + unsigned char *b = &text[pos]; + + for (len = 0; len < max && *a++ == *b++; len++); + } + + if (len > matchlen) { + matchpos = scan_beg; + if ((matchlen = len) == max) { + break; + } + } + } + scan_pos = prev[scan_pos & (dicsiz - 1)]; + scan_beg = scan_pos - off; + } + + if (chain >= LIMIT) + too_flag[h] = 1; + +---------------------------------------------------------------------------- + + |-- a ---| |--- b --| + +---------------+--------+--------------------+--------+--------+ +text | | x'| | | | | | |x | | | | + +---------------+--------+--------------------+--------+--------+ + ^ \ \ \ \ + | scan_beg scan_pos pos pos+off + scan_end + + |----| + scan_beg の有効範囲 + + |----------------- dicsiz ------------------| + +---------------------------------------------------------------------------- -細部は未だ目をつぶっているのだがこれで match_insert() の解読は終りだ。 +scan_beg, scan_end の範囲もわかりやすいし、hash[h] が NIL の場合の処理 +も明示的だ。この書き換えを行う場合、scan_beg が負の値になる可能性があ +る。元もとの処理では scan_end 等の変数を unsigned にしているので、これ +らを int にして while 条件で負の scan_beg をはじかなければならないこと +に注意。そうすると、scan_beg != NIL は必要なくなるのだがわかりやすさを +追求した。 -match_insert() の処理とは以下の通りだ。 +これで match_insert() の解読は終りだ。match_insert() の処理とは以下の +通りだ。 ---------------------------------------------------------------------------- match_insert() は、text[pos] から始まる文字列に一致する文字列を辞書 から検索し、見つかった位置と一致長を matchpos, matchlen に設定する。 - もし、最長一致文字列が見つからなければ matchpos は、off に設定され、 - matchlen は更新されない。 + もし、最長一致文字列が見つからなければ matchpos は、pos に設定され、 + matchlen は更新されない。(実は、matchpos = pos の情報は特に使われてない) 見つかった場合、matchlen は呼び出し前の matchlen よりも大きくなる。 (呼び出し前では matchlen の意味は最低限一致しなくてはならない文字列 @@ -1986,6 +2082,521 @@ の文字列のそれぞれで辞書を検索し、より長く見つかった方を使う」という最 適化を行っている事がわかってしまった後は解読は簡単だった。(実はこの事 実も教えてもらった事だ。全然ダメじゃん) + +さて、これで一通りの解析は済んだわけだが、ここまでの解析内容を読み直し +てみると、以下の点がまだひっかかる。 + +1. ハッシュ関数は最適なのか?特に HSHSIZ{2^15} は最適なのか? +2. too_flag[] は、実際に照合を行いループがLIMITを越えた場合に + 設定される。しかし、ハッシュのチェーンを作る際にチェーンの + 個数をあらかじめ数えておけば一度の探索すらも行われず。より + 早く処理されないだろうか? + +現状、1, 2 とも実施してみたところ特に速度の改善は見られなかった。特に +1 は、微妙なところがありほとんどの書き換えは性能を悪くするだけだった。 +なかなか興味深いものがある。 + +これは今後の課題としていずれまた検証しよう。そろそろ slide.c も飽きて +きたのでひとまずはこれで終りにしたい。 + + +bit 入出力ルーチン (crcio.c) +--------------------------- + +これから Huffman 法の解読に移るのだが、その前準備として bit 入出力ルー +チンの解読を行う。Huffman 法の実装では必ず bit 入出力処理が必要になる。 +LHa for UNIX ももちろん例外ではなく、Huffman 法の実装を解読するにあた +りこの部分の処理内容ははっきりさせておいた良いと考えたのだ。 + +LHa for UNIX version 1.14i では bit 入出力ルーチンは crcio.c で定義さ +れている。(このようなファイル名に存在するのは意外な事だ。最近の LHa +for UNIX では、私が bitio.c というファイルを設け、bit 入出力ルーチンは +そこに切り出した) + +crcio.c のうち bit 入出力ルーチンは fillbuf(), getbits(), putcode(), +putbits(), init_getbits(), init_putbits() の 6 関数だ。 + +まず、初期化用の init_getbits(), init_putbits() を見よう。 + +void +init_getbits( /* void */ ) +{ + bitbuf = 0; + subbitbuf = 0; + bitcount = 0; + fillbuf(2 * CHAR_BIT); +#ifdef EUC + putc_euc_cache = EOF; +#endif +} + +void +init_putbits( /* void */ ) +{ + bitcount = CHAR_BIT; + subbitbuf = 0; + getc_euc_cache = EOF; +} + +それぞれ bit 入力、bit 出力を行うための初期化処理だ。CHAR_BIT というの +は 8 で、char の bit サイズを表しているらしい。詳細はわからないが初期 +状態はとにかくこれだ。ここで使用されている変数は、 + +static unsigned char subbitbuf, bitcount; + +が、crcio.c で定義されており、 + +EXTERN unsigned short bitbuf; + +が、lha.h で定義されている(EUC なんたらは本質ではない無視しよう)。グロー +バル変数と言うのは忌むべきものだが、とにかく使用されている変数と初期値 +を確認したので次に移ろう。init_getbits() で、早速 fillbuf() が呼ばれて +いる。この処理内容を見る。 + +void +fillbuf(n) /* Shift bitbuf n bits left, read n bits */ + unsigned char n; +{ + /* (A) */ + while (n > bitcount) { + n -= bitcount; + /* (B) */ + bitbuf = (bitbuf << bitcount) + (subbitbuf >> (CHAR_BIT - bitcount)); + /* (C) */ + if (compsize != 0) { + compsize--; + subbitbuf = (unsigned char) getc(infile); + } + else + subbitbuf = 0; + bitcount = CHAR_BIT; + } + /* (D) */ + bitcount -= n; + bitbuf = (bitbuf << n) + (subbitbuf >> (CHAR_BIT - n)); + subbitbuf <<= n; +} + +まず、初期状態として + + bitbuf = 0; + subbitbuf = 0; + bitcount = 0; + +であり、fillbuf の引数 n には 2 * CHAR_BIT が与えられたのだった。いき +なり while 条件を満たすのでループ部の解析を行わなくてはならなくなるが、 +ひとまずこれを無視して最後の 3 行 (D) に着目する。条件は少ない方が良い +のだ。 + + /* (D) */ + bitcount -= n; + bitbuf = (bitbuf << n) + (subbitbuf >> (CHAR_BIT - n)); + subbitbuf <<= n; + +bitbuf << n, subbitbuf << n されているので、bitbuf, subbitbuf を n ビッ +ト左にずらす処理のようだ。さらに bitbuf には、subbitbuf を n ビットず +らしたときに溢れた部分を bitbuf にセットしている。っと、 + + (subbitbuf >> (CHAR_BIT - n)) + +の部分を軽く説明したが、図示して確認しておこう。 + +subbitbuf は unsigned char なので 8 bit の変数だ。 + +---------------------------------------------------------------------------- + 7 6 5 4 3 2 1 0 + +--+--+--+--+--+--+--+--+ + subbitbuf | | + +--+--+--+--+--+--+--+--+ + <-- n --> +---------------------------------------------------------------------------- + +n が例えば 3 の場合、CHAR_BIT - n は、5 だから subbitbuf を 5 ビット右 +にずらした値を取っている。つまり、図の 7, 6, 5 ビット目が一番右に来る +ようになっており、この値を bitbuf に足している。(C言語では、unsigned +な変数を右にシフトすると上位ビットには 0 が入る) + +fillbuf() の後半 3 行(いや、後半2行か)は。結局 bitbuf と subbitbuf を +一つの bitbuf とみなして n ビット左にずらしていることがわかる。 + +---------------------------------------------------------------------------- +<ビットバッファ全体図 (予想)> + + 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + bitbuf | | x y z| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + \ <-- n -> + subbitbuf + <-------------- bitcount -------------> + +---------------------------------------------------------------------------- + +このとき、当然図の x, y, z の部分(n = 3 は例としての値だ)が空く事になる。 +bitcount という変数が n 引かれていたが、これは bit バッファ全体の有効 +なビット数を表しているのではないかと予想しておく。すなわち図の状態なら +21 だ。while ループは(関数名から)この空き部分を埋める処理なのではない +かと適当に予想できる。では、while ループを見よう。もう一度初期値を確認 +し、最初に行われる処理内容を見よう。 + +最初、 + + bitbuf = 0; + subbitbuf = 0; + bitcount = 0; + +であるから、bitバッファは空っぽだ。当然 fillbuf(2 * CHAR_BIT) されると +while 条件を満たす。きっと 16 bit だけ bitバッファが補充されるはず(つ +まり、bitbuf いっぱい、subbitbuf 空)だ。 + + /* (A) */ + while (n > bitcount) { + n -= bitcount; + +で、ビットバッファが保持する bit 数以上を要求されたので、ループに入る。 +n -= bitcount で、本当に足りない部分が何ビットなのかを得ている。ここで +は 16 だ。次の行 + + /* (B) */ + bitbuf = (bitbuf << bitcount) + (subbitbuf >> (CHAR_BIT - bitcount)); + +これは先程も出て来た処理だ。ビットバッファ全体を bitcount 分左にずらし +ている(ただし、まだ subbitbuf はずらされていない)。この時点で予想が少 +し覆された。8 - bitcount で subbitbuf をずらしているから bitcount は最 +大 8 の値しか持たないだろうということだ。どういうことか、考えてみる・・・ +考えてもわからなかったので次に進もう。 + + /* (C) */ + if (compsize != 0) { + compsize--; + subbitbuf = (unsigned char) getc(infile); + } + else + subbitbuf = 0; + bitcount = CHAR_BIT; + +compsize というのが出て来たが、この値がどうあろうとも subbitbuf は8 ビッ +ト埋められ。bitcount は 8 に設定されている。わかった bitcount は、 +subbitbuf に保持する bit 数だ。図を訂正しよう。 + +---------------------------------------------------------------------------- +<ビットバッファ全体図> + + 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + bitbuf | | x y z| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / <-- n -> + subbitbuf + <--------> + bitcount + +---------------------------------------------------------------------------- + +この図を踏まえてもう一度初期状態での処理内容を追いかける。 + +まず、(A) で、subbitbuf は空なので、bitcount は 0 だ。要求した bit 数 +n{16} より小さいのでループに入る。n は 16 のままだ。 + +(B) で、subbitbuf に残っている bit を bitbuf にずらしている。今はまだ +空なのでbitbuf はここでもまだ空だ。 + +(C) で、8 ビットファイルからデータを読む(compsize は常に0でないと考え +る)。bitcount は 8 になる。この時点で bitバッファ全体は subbitbuf だけ +値が入った状態になる。 + +次のループに移ろう。(A) で、subbitbuf はいっぱいであるが要求した n{16} +よりは小さいので、まだループは続く。n はここで 8 になる。 + +(B) で、subbitbuf に残っている bit (8 bit だ)を bitbuf にずらしている。 +今度は subbitbuf 全体が bitbuf に移っているのと同じだ。(つまり、bitbuf += subbitbuf) + +(C) で、また subbitbuf は 8 bit 補充される。 + +(A) で、n{8} > bitcount{8} は偽なのでループが終る。 + +(D) で、subbitbuf に残っている bit はすべて bitbuf に移る。bitbuf は 16 +bit いっぱいになる。bitcount は 0 だ。 + +この処理結果から fillbuf(n) は、bitbuf に n ビット読み込む処理だと言え +る。引数に指定できる n が最大 16 ビットであることにも気づいて良いだろ +う。処理内容を確認してみればわかる。 + +ここで、subbitbuf の用途に気づいた。ファイルからの読み込みが 8 ビット +単位でしかできないので、それを補うための先読みバッファであろう。例えば +1 ビットだけ bitbuf を fill したければ subbitbuf に 7 bit 残し、1 bit +だけ bitbuf に設定される(確認してみればわかる) + +fillbuf() がわかったので、それを利用している getbits() の内容を確認し +よう。 + +unsigned short +getbits(n) + unsigned char n; +{ + unsigned short x; + + x = bitbuf >> (2 * CHAR_BIT - n); + fillbuf(n); + return x; +} + + x = bitbuf >> (2 * CHAR_BIT - n); + +は、3 度も出て来たので + + buf >> (sizeof(buf)*8 - n) + +を buf の上位 n ビットを得る式だとしてマクロにしても良いだろう。(が、 +良い名前が思い付かないのでそうはしない)。とにかく、bitbuf の上位 n ビット +を下位 n ビットとして x に代入している。その後で、 + + fillbuf(n); + +している。n bit を x に渡したので bitbuf から上位 n ビットを捨てて、n +ビット補充する。ここで、bitbuf は常にいっぱいの状態になっていることが +わかる。(ファイルの末尾付近の場合、正確に bitbuf に何ビット残っている +かが判断できないが、種明かしをするとこのことは LHa の処理内容にとって +はどうでもいいことだ。getbits() は decode 処理で使われるのだが、decode +処理は何ビットの情報を decode する必要があるかを他の情報からあらかじめ +得ている) + +次に移ろう今度は putcode() だ。put の場合まずは、init_putbits() で +初期化が行われている。その値は以下だ。 + + bitcount = CHAR_BIT; + subbitbuf = 0; + getc_euc_cache = EOF; + +getc_euc_cache は無視だ。bitcount と subbitbuf の値が設定され、bitbuf +は利用されない。先程とは違い subbitbuf が空なのにbitcount が 8 なので、 +bitcount の使われ方が多少異なるようだ。get の場合は、bitcount は、 +subbitbuf に保持する bit 数だったが今度は subbitbuf の空き bit 数だろ +うと予想しておこう。 + +そして、putcode(n, x) を見る。実はソースを見るとわかるのだが、もう一つ +の出力ルーチン putbits() は、putcode() の呼び出しに書き換え可能だ。 +putbits() は、 + +void +putbits(n, x) /* Write rightmost n bits of x */ + unsigned char n; + unsigned short x; +{ + x <<= USHRT_BIT - n; + putcode(n, x); +} + +っと書き換えられるのだ。なので、putcode() の内容を先に確認するわけだ。 + +void +putcode(n, x) /* Write rightmost n bits of x */ + unsigned char n; + unsigned short x; +{ + /* (A) */ + while (n >= bitcount) { + n -= bitcount; + /* (B) */ + subbitbuf += x >> (USHRT_BIT - bitcount); + x <<= bitcount; + /* (C) */ + if (compsize < origsize) { + if (fwrite(&subbitbuf, 1, 1, outfile) == 0) { + /* fileerror(WTERR, outfile); */ + fatal_error("Write error in crcio.c(putcode)\n"); + /* exit(errno); */ + } + compsize++; + } + else + unpackable = 1; + subbitbuf = 0; + bitcount = CHAR_BIT; + } + /* (D) */ + subbitbuf += x >> (USHRT_BIT - bitcount); + bitcount -= n; +} + +処理内容が fillbuf() のときと似ている。まずは、先程と同様に while 条件 +を無視して考えてみる。(D) だ。 + + /* (D) */ + subbitbuf += x >> (USHRT_BIT - bitcount); + bitcount -= n; + +この式はもう 4 度目だ。まず、x の上位 bitcount ビットを得て、subbitbuf +に足している。bitcount は、先程 subbitbuf の空きであろうと予想したが、 +n 引かれているので、埋めた分空きが減っているわけだ。予想は当たっている +だろう。この時点でこの関数が x の上位ビットを利用することがわかる。コ +メントに rightmost n bits of x と書かれているが惑わされてはいけない。 +多くの場合、コメントはせいぜいヒントとしての情報でしかない。信用しては +いけないものなのだ。(コメントはあまりデバッグされない。コメントが詳し +ければ詳しい程コメントはエンバグしやすい。疑ってかかろう。これは本書に +も言える。すべてを鵜のみにしてはいけないのだ) + +では、処理内容に移る。まずは (A) + + /* (A) */ + while (n >= bitcount) { + n -= bitcount; + +subbitbuf の空きが n 以下であればループに入る。subbitbuf 一つではn ビッ +ト全部は賄えないからループで小刻みに処理しようということだろう(もう全 +体の処理内容の予想はついている) +n から bitcount 引いているので、n ビットのうちこれから bitcount 分は +処理されることをここでさっさと記録して次のループに備えている。 + + /* (B) */ + subbitbuf += x >> (USHRT_BIT - bitcount); + x <<= bitcount; + +x の上位 bitcount ビットを subbitbuf に足している。subbitbuf の空きが +これで埋まった。subbitbuf はもういっぱいだ。x を bitcount シフトするこ +とで subbitbuf に渡した x の上位ビットを捨てている。 + + /* (C) */ + if (compsize < origsize) { + if (fwrite(&subbitbuf, 1, 1, outfile) == 0) { + /* fileerror(WTERR, outfile); */ + fatal_error("Write error in crcio.c(putcode)\n"); + /* exit(errno); */ + } + compsize++; + } + else + unpackable = 1; + subbitbuf = 0; + bitcount = CHAR_BIT; + +compsize は無視しても良い。処理の本質ではないからだが、すぐにわかるので +一応説明すると、 + if (compsize < origsize) { + ... + else + unpackable = 1; +で、圧縮ファイルサイズが元のファイルサイズを上回ったときに +処理を終るようになっている(unpackable = 1 して、他の箇所でこの変数を監視する。 +unpackable == 1 なら処理を中断する) + +とにかく (C) の時点では必ず subbitbuf がいっぱいになるので 1 バイトを +ファイルに書き出している。その後、subbitbuf = 0, bitcount = 8 として +subbitbuf を空けて次のループに備えている。 + +もういいだろう。putcode() は、論理的には x のうち上位 n ビットを出力す +る処理だ。引数 n の上限が x の最大ビットサイズ 16 になるのは説明するま +でもないだろう。 + +putcode() は実装として、subbitbuf と x を一つに繋げて n bit 左にずらし +ている処理だと考えても良い。そうして、subbitbuf がいっぱいになったらそ +の都度(1 バイトずつ)ファイルに書き出すのだ。 + +---------------------------------------------------------------------------- +<ビットバッファ全体図> + + <--- 左にずらす + + 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |* * * |x y z | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / / <-n-> + subbitbuf x + <--------> + bitcount + +---------------------------------------------------------------------------- + +putbits() も見よう。先程 putcode() の呼び出しに書き換えたコードを見ると +すぐわかるが、 + + x <<= USHRT_BIT - n; + putcode(n, x); + +最初の式で、x の下位 n ビットを x の上位 n ビットにしている。 +そうして、putcode() を呼び出しているので、putbits(n, x) は、x +の下位 n ビットを出力する処理だ。 + +以上でビット入出力ルーチンは終りだ。出力に関して一つ捕捉しておくと +putcode(), putbits() では最後の最後で subbitbuf に情報が残ったままファ +イルに書き出されない状態になる。これを吐き出すために利用者は + + putcode(7, 0) + +を行う必要がある。 + +まとめよう + +---------------------------------------------------------------------------- +fillbuf(n) + bitbuf から上位 n ビットを捨てて、下位 n ビットをファイルから読み込 + み埋める。 + +getbits(n) + bitbuf の上位 n ビットを下位 n ビットとして返す。bitbuf は n ビット + 補充される。 + +putcode(n, x) + x の上位 n ビットをファイルに出力する。最後の出力時は putcode(7, 0) + する必要がある。 + +putbits(n, x) + x の下位 n ビットをファイルに出力する。最後の出力時は putcode(7, 0) + する必要がある。 + +init_getbits() + 入力処理の初期化 + +init_putbits() + 出力処理の初期化 +---------------------------------------------------------------------------- + +読み込みに関して、bitbuf のサイズが 16 ビットで常にその状態が保持され +ているのは LHa にとって重要な事だ。decode 処理では直接 bitbuf を参照す +る箇所がある。 + +Huffman 法 (huf.c) +------------------ + +LHa for UNIX では、静的 Huffman 法として、shuf.c、動的 Huffman 法とし +て dhuf.c があるらしいが、これらに関しては触れない。LHa では、これらは +過去の版のアーカイブを解凍できるように decode のみサポートしているよう +だ。そこで、まずは -lh4-, -lh5-, -lh6-, -lh7- で利用されている huf.c +の解析を中心に行うこととしたい。 + +---------------------------------------------------------------------------- +構造 + + +-----------+ + | blocksize | + +-----------+ + 16bit + + +-----+--------------------+ + | len | 符号化した pt_len |c_lenのハフマン符号表 + +-----+--------------------+ + 5bit ?? bit + TBIT + + +-------+------------------+ + | len | 符号化した c_len | 文字と長さのハフマン符号表 + +-------+------------------+ + 9bit ?? bit + CBIT + + +---------+--------------------+ + | len | 符号化した pt_len | 位置のハフマン符号表 + +---------+--------------------+ + pbit ?? bit + + +---------------------+ + | 圧縮文 | + +---------------------+ + + pbit=14bit(lh4,5) or 16bit(lh6) or 17bit(lh7) +---------------------------------------------------------------------------- # Local Variables: # mode : indented-text -- 新井康司 (Koji Arai)