読者です 読者をやめる 読者になる 読者になる

DoubleArray(3-1): TAIL配列圧縮

common lisp algorithm

DoubleArrayの3-1。
今回はTAIL配列の圧縮を行う。


参考にした(かな?)のは、次の論文: 『ダブル配列におけるキャッシュの効率化』*1

上の論文の中に「後方一致する接尾辞を併合することで、TAILを圧縮することができる」という記述があるが、今回はこの通りのことを行っている。

実装

;; 型定義
(deftype octet () '(unsigned-byte 8))

;; BASE配列のindexから、TAIL配列のindexを取得
(defun tail-index (base-index)
  (- @*base*#base-index))

;; 接尾文字列の長さを取得  ※ 終端文字(値)は、'1'
(defun tail-length (base-index &aux (index (tail-index base-index))) 
  (- (position 1 *tail* :start index) index))

;; TAIL配列へのポインタ(index)を保持するBASE配列のindexを集める
(defun collect-terminal-base-index ()
  (loop FOR i FROM 0 BELOW (length *base*)
        WHEN (minusp @*base*#i)  ; 負数なら、TAILへのポインタ(index)
        COLLECT i))

;; tail-from-end-=<とsharable?で共通して用いるループ
;; base-index1とbase-index2が指す接尾文字列を、それぞれ後ろ側から辿っている
(macrolet ((common-loop (&body body)
             `(let ((t1 (tail-index base-index1))
                    (t2 (tail-index base-index2)))
                (do ((i1 (1- (tail-length base-index1)) (1- i1))
                     (i2 (1- (tail-length base-index2)) (1- i2)))
                    ((minusp i1) t) 
                  ,@body))))

  ;; base-index1とbase-index2が指す接尾文字列を、末尾を起点に比較する
  (defun tail-from-end-=< (base-index1 base-index2)
    (common-loop
      (cond ((minusp i2) (return nil))
            ((< @*tail*#(+ t1 i1) @*tail*#(+ t2 i2)) (return t))
            ((> @*tail*#(+ t1 i1) @*tail*#(+ t2 i2)) (return nil)))))

  ;; base-index1(の接尾文字列)がbase-index2(の...)に包含されているか(共有可能か?)どうかを調べる
  (defun sharable? (base-index1 base-index2)
    (common-loop
      (when (or (minusp i2)
                (/= @*tail*#(+ t1 i1) @*tail*#(+ t2 i2)))
        (return nil)))))

;; 新しいTAIL配列に、(旧TAIL配列==*tail*の)tail-indexから始まる末尾文字列を追加する
(defun append-to-tail (new-tail tail-index)
  (do ((i tail-index (1+ i)))
      ((= 1 @*tail*#i) (vector-push-extend 1 new-tail))
    (vector-push-extend @*tail*#i new-tail)))


;; TAIL配列の圧縮を行う関数
;; 引数はdouble-array構造体
(defun shrink-tail (&optional (*da* *da*))
         ;; TAIL配列へのポインタ(index)を含むBASE配列のindexリスト
         ;; 併合処理を行いやすくするために、接尾文字列によって(逆順に)ソートしておく
  (let ((#1=base-indices 
         (sort (collect-terminal-base-index) (complement #'tail-from-end-=<)))

         ;; 新しいTAIL配列
        (new-tail (make-array 128 :fill-pointer 1 :adjustable t :element-type 'octet)))
    ;; base-indicesの要素を順番に処理
    (loop AS (bi share?) IN (cons `(,(first #1#) nil)
                                  (mapcar (lambda (cur prev) 
                                             `(,cur ,(sharable? cur prev)))
                                          (rest #1#) #1#)) DO
      (let ((fpos (fill-pointer new-tail)))
        (if share?
            ;; 共有可能なら、TAIL配列へのポインタを修正するだけにする
            (setf @*base*#bi (- (1+ (tail-length bi)) fpos)) ; 1+ は終端文字(値)用
          (progn 
            ;; 共有不可能なら、new-tailに末尾文字列を追加し、TAIL配列へのポインタも修正する
            (append-to-tail new-tail (tail-index bi))
            (setf @*base*#bi (- fpos))))))

    ;; TAIL配列更新
    (setf *tail* new-tail)
    'DONE))


;; 実行
> (shrink-tail *da*)
--> DONE

shrink-tail関数を適用することで、TAIL配列のサイズを半分くらいに圧縮できるようだ(もちろん、その時にTAIL配列の内容によって変わるので、あくまでも半分はおおまかな目安だが)

*1:矢田晋,森田和宏,泓田正雄,平石亘,青江順一.ダブル配列におけるキャッシュの効率化. FIT2006.pp. 71-72.2006.