bzip2を読むタイトル__2_

bzip2を読む MTF3

こんにちは、junkawaです。

本記事では、Move To Front (MTF) 変換についてソースコードを交えて紹介します。

generateMTFValues() MTF変換部分

本記事では、generateMTFValues() 関数の MTF変換部分について紹介します。

generateMTFValues()@compress.c
https://github.com/junkawa/bzip2/blob/master/bzip2-1.0.6/compress.c#L120

https://github.com/junkawa/bzip2/blob/master/bzip2-1.0.6/compress.c#L163

https://github.com/junkawa/bzip2/blob/master/bzip2-1.0.6/compress.c#L231

  163:    for (i = 0; i < s->nblock; i++) {
  164:       UChar ll_i;
  165:       AssertD ( wr <= i, "generateMTFValues(1)" );

ソート済みブロック番号 UInt32 ptr[i] を読み出して、MTF変換して UInt16 mtfv[wr] に書き出します。

アサートの意味としては、ptr と mtfv は同じメモリ領域を参照しているため、wr > i となると、ptr から読み出す前に mtfv へ書き込んでしまい、まだ読み出していない ptr の領域を破壊してしまう、ということだと思います。

しかし ptr[i] は UInt32型で、mtfv[wr]は UInt16型です。したがって、wr*2 <= i が正しいアサート条件ではないでしょうか。

bzip2-0.9.0でも同様の型で、同様のアサート条件になっていました。

備考

ランレングス符号化を考慮すると、連続するシンボル’はさらに短いバイト数に変換されます。したがって、実際の書き込み数 <= wr*2 <= i 、となります。

  166:       j = ptr[i]-1; if (j < 0) j += s->nblock;

ptr[i] はソートされた、i番目に小さい行の先頭ブロック番号です。

ここで、ブロックソート変換の出力は、巡回行列の行のソート後の、「最後の列」です。

各行は入力データブロックを1シンボルずつずらして生成され、末尾は入力データを巡回して埋められます。

したがって、各行の最後のシンボルは、block[ ptr[i] - 1 ] で得られます。

また、ptr[i]が0の場合、ptr[i]-1は-1となるので、巡回して s->nblock-1 としています。

図より、「巡回行列の行番号-1」が各行の末尾のシンボルを指すことが分かります。

一番上の行番号10では、末尾 r は、block[10-1] の block[9] となります。
ptrを使ってソート順に考えると、先頭は ptr[0] で、行番号10です。
したがって、ソート順で先頭の行の末尾は、block[ ptr[0] - 1 ] となります。

  167:       ll_i = s->unseqToSeq[block[j]];

block[j] は、ブロックソート変換後のi番目のシンボルです。これを連続シンボル’ ll_i に変換します。

  168:       AssertD ( ll_i < s->nInUse, "generateMTFValues(2a)" );
  169: 

シンボル’ は 0〜s->nInUse-1なので、ll_i < s->nInUse を確認しています。

  170:       if (yy[0] == ll_i) { 
  171:          zPend++;

yy[0] は1回前のループのll_iです。204行目でセットされます。また、初期値は0です。

前回と今回のll_iが同じ場合、つまり2回連続で同じシンボル’ が続いた場合、zPendをインクリメントします。ループ初回時は、ll_iが0の場合にzPendをインクリメントします。

zPendは、シンボル’の連続回数-1となります。

例) 2が、4回連続(2,2,2,2)の時、zPend = 3

  172:       } else {
  173: 

このブロックは、前回のシンボル’ と今回のシンボル’ ll_i が異なる場合に実行されます。

  174:          if (zPend > 0) {
  175:             zPend--;
  176:             while (True) {
  177:                if (zPend & 1) {
  178:                   mtfv[wr] = BZ_RUNB; wr++; 
  179:                   s->mtfFreq[BZ_RUNB]++; 
  180:                } else {
  181:                   mtfv[wr] = BZ_RUNA; wr++; 
  182:                   s->mtfFreq[BZ_RUNA]++; 
  183:                }
  184:                if (zPend < 2) break;
  185:                zPend = (zPend - 2) / 2;
  186:             };
  187:             zPend = 0;
  188:          }

zPendが1以上、つまり前回までに何回か連続で同じシンボル’が続いた場合、ここでランレングス符号化してmtfvに出力します。

本来のMTF符号化では、入力データが連続する場合、連続する0値に符号化します。bzip2では入力データが連続している場合、ランレングス符号化(Run Length Encoding, RLE) で符号化します。

入力データが値3で10回連続する(zPend=9)場合、下記となります。

入力データ [3,3,3,3,3,3,3,3,3,3]
bzip2のMTF+RLE [0,1,0] (下記参照)

BZ_RUNA、BZ_RUNB は、bzlib_private.hで定義されます。

bzlib_private.h

  118: #define BZ_RUNA 0
  119: #define BZ_RUNB 1

mtfv[]は出力データ、wrは書き出すデータ位置。s->mtfFreq[i]はデータiの出現回数(後でハフマン符号化時に使用)です。

上記処理のランレングス符号化の結果を記します。

RLE符号化後の列の表記 [x1,x2,x3] は、mtfv[wr++] = x1、mtfv[wr++] = x2、mtfv[wr++] = x3 となるような値の出力結果です。

ランレングス符号化は様々な実現方法がありますが、ここでの方法はゼロレングスエンコーディング(Zero Length Encoding, ZLE) です。

ZLEについては、下記サイトを参考にしました。
いつも勉強させていただいている、 広井 誠さんのホームページです。

正の整数を 2 進数で表す場合、最上位ビットは常に 1 なるので省略することが可能です。Zero Lenght Encoding では、個数 N に 1 を加えて最上位以外のビットを出力しています。
http://www.geocities.jp/m_hiroi/light/pyalgo29.html

引用文中の個数Nが、zPendとなります。

zPendに1を加えた数は、シンボルの連続回数です。

このシンボルの連続回数を2進表現にして、最上位ビット以外のビット(表中の下線のついた部分)を下位ビットから出力したものが、RLE符号化後の値となります。

  189:          {
  190:             register UChar  rtmp;
  191:             register UChar* ryy_j;
  192:             register UChar  rll_i;
  193:             rtmp  = yy[1];
  194:             yy[1] = yy[0];
  195:             ryy_j = &(yy[1]);
  196:             rll_i = ll_i;
  197:             while ( rll_i != rtmp ) {
  198:                register UChar rtmp2;
  199:                ryy_j++;
  200:                rtmp2  = rtmp;
  201:                rtmp   = *ryy_j;
  202:                *ryy_j = rtmp2;
  203:             };
  204:             yy[0] = rtmp;
  205:             j = ryy_j - &(yy[0]);
  206:             mtfv[wr] = j+1; wr++; s->mtfFreq[j+1]++;
  207:          }
  208: 
  209:       }

この処理でやりたいことは下記のとおりです。

 ・リストyyから ll_i の位置を取得し、mtfv[wr] にセットする
 ・ll_i をリストyyの先頭に移動する

190〜192行でローカル変数 rtmp、ryy_j、rll_i を定義しています。変数名の先頭にrがついているのは、変数が register 修飾子付きで宣言されているからだと思われます。

193,194行でリストyyの先頭(yy[0])の値を1番目(yy[1])に入れます。

195行目でポインタ ryy_j にリストの1番目のアドレス(&(yy[1]))を入れます。
このポインタ ryy_j を上手く使って、リスト中のll_iの位置を取得します。

196行目で ll_i を rll_i にセットしています。これは register 修飾子で宣言した変数 rll_i を使うことで高速化を狙ったものだと思われます。

197行目では、変換対象のシンボル’ rll_i が、リスト yy 中で見つかるまで(while (rll_i != rtmp))、リストを辿っています(ryy_j++)。rtmpは今見ている、リスト中の値です。

また、リストを辿るついでに、要素を後ろに一つずつずらし、セットしていきます。202行目で、今見ているrtmp2を、次に見るリストの位置ryy_jにセットします。

204行目でyy[0] に変換対象の値ll_i(whileループを抜けたときにはrtmpはll_iと同じ)をセットしています。

whileループを抜けた時点で、ryy_jは、値ll_iがあったyy中の場所を指しています。

MTFでは、対象の値(ll_i)を入力とし、リストyyの先頭からの位置を出力とします。
先頭からの位置は、値ll_iのあったリスト要素を指すアドレスryy_jから、リストの先頭アドレス&(yy[0])を引いた値となります。

205行目で、j = ryy_j - &(yy[0]) としてMTF変換後の値 j を求めています。

yy は Uchar型の配列であるため、アドレスの差分がそのまま要素の番号となります。

206行で出力データ(mtfv[wr])、出現回数(s->mtfFreq[j+1])をセットします。

mtfv[wr] = j+1; の +1 について説明します。

j = ryy_j - &(yy[0]); かつ、ryy_jの初期値は&(yy[1])なので、jは1以上の値となります。

mtfv[]には0以上の値が入りますが、0と1はゼロランレングスでのRUNAとRUNBに使用されているため(予約されているため)、0、1以外の値を使う必要があります。

本来のMTFにて0は、前回と同じ値が出た時。1は、今回の値がリストの先頭から1番目 (yy[1])に出た時となります。

ゼロランレングスにて前回と同じ値が出た時に0(RUNA)と1(RUNB)を使用しているので、bzip2では、リストの先頭からの距離に1を加えた値を出力するようにしています。

もちろん伸長ではこれを意識して処理する必要があります。具体的には、伸長処理のdecompress.c:427で対応しています。
https://github.com/junkawa/bzip2/blob/master/bzip2-1.0.6/decompress.c#L427

  210:    }
  211: 
  212:    if (zPend > 0) {
  213:       zPend--;
  214:       while (True) {
  215:          if (zPend & 1) {
  216:             mtfv[wr] = BZ_RUNB; wr++; 
  217:             s->mtfFreq[BZ_RUNB]++; 
  218:          } else {
  219:             mtfv[wr] = BZ_RUNA; wr++; 
  220:             s->mtfFreq[BZ_RUNA]++; 
  221:          }
  222:          if (zPend < 2) break;
  223:          zPend = (zPend - 2) / 2;
  224:       };
  225:       zPend = 0;
  226:    }
  227: 

ここの処理は174〜188行と全く同じです。

174〜188行は、連続した値の後で、異なる値があった場合に入る処理なので、連続した値の後で、異なる値を検出しない場合、ランレングス符号化されないまま163〜210行のforループを抜けてしまいます。

ここでは、入力データブロックの最後が連続していた場合に、それらをランレングス符号化します。

  228:    mtfv[wr] = EOB; wr++; s->mtfFreq[EOB]++;
  229: 

出力データの最後に、EOBをセットします。

EOBは、s->nInUse+1 です。

s->mtfFreq[EOB]++で、シンボルEOBの出現回数を記録しています。EOBもハフマン符号化の対象であるためです。

  230:    s->nMTF = wr;

出力データの長さ(wr)をs->nMTFに保存します。

これで、MTF符号化が完了します。

備考

伸長時のMTF逆変換に必要な情報は下記となります。

 ・mtfv[0..wr-1]
 ・s->inUse

s->inUseから、seqToUnseq(s->unseqToSeqの逆)、s->nInUse、EOBを導出できます。

伸長時のリストyyは、for(i=0; i<s->nInUse; i++) yy[i] = i; で生成できます。

データからEOBが見つかるまでMTF逆変換を行い、生成したデータ(シンボル’)をseqToUnseqによってシンボルに変換します。

伸長時のMTF逆変換については、別の記事で紹介します。
decompress.c の 423 でMTF逆変換を行っています。
https://github.com/junkawa/bzip2/blob/master/bzip2-1.0.6/decompress.c#L423

  231: }

まとめ

MTF変換の実装について紹介しました。

次回は、ハフマン符号について紹介します。

ご覧下さりありがとうございます。いただいたサポートは図書館への交通費などに使わせていただきます。