2015-05-09-01-withマクロの改善 - project-enigma

2015-05-09-01-withマクロの改善

>> Site top >> weblog >> 月別アーカイブ >> 2015年05月のlog >> 2015-05-09-01-withマクロの改善

最終更新日付:2015/05/09 23:59:00


withマクロの改善

2015 年 05 月 09 日

前回の with マクロはパフォーマンスに問題があった。今回はそれを改善する措置について。

まずは、tuple:get を毎回使って1億回ループした場合の結果から見るとしよう。以下のようになった。

(let ((obj (tuple:create :tag "foo" :counter 0)))
  (time (progn
          (locally (declare (optimize speed))
            (do ((i 0 (1+ i)))
                ((<= 100000000 i) nil)
              (declare (type fixnum i))
              (incf (tuple:get obj :counter))))
          (values))))

;Evaluation took:
;  2.511 seconds of real time
;  2.511616 seconds of total run time (2.511616 user, 0.000000 system)
;  100.04% CPU
;  8,532,930,700 processor cycles
;  0 bytes consed

 

前回の with マクロでは、 . を使用したメンバ参照が全て tuple:get 呼び出しに展開されるため、これまでの with マクロを使った場合も上記と同じと思っていいだろう。そして以下は今回のパフォーマンス改善をした後の tuple:with を使った結果だ。

(let ((obj (tuple:create :tag "bar" :counter 0)))
  (time (tuple:with obj
            (locally (declare (optimize speed))
              (do ((i 0 (1+ i)))
                  ((<= 100000000 i) nil)
                (declare (type fixnum i))
                (incf obj.counter)))
            (values))))

;Evaluation took:
;  1.607 seconds of real time
;  1.606811 seconds of total run time (1.606811 user, 0.000000 system)
;  100.00% CPU
;  5,438,942,157 processor cycles
;  0 bytes consed

 

1億回でこれなので大した違いではないかもしれないが、まぁ改善とは言えるだろう。で、tuple:with はどう変わったのか。まず、以下は前回も提示した従来の展開結果だ。

(tuple:with (pt1 pt2)
  (sqrt (+ (expt (- pt1.x pt2.x) 2)
           (expt (- pt1.y pt2.y) 2))))

   ; old expansion
=> (symbol-macrolet ((pt1.x (tuple:get pt1 :x))
                     (pt2.x (tuple:get pt2 :x))
                     (pt1.y (tuple:get pt1 :y))
                     (pt2.y (tuple:get pt2 :y)))
     (sqrt (+ (expt (- pt1.x pt2.x) 2)
              (expt (- pt1.y pt2.y) 2))))

 

これが、今回の変更を加えた後には以下のように展開される。細かい話は後だ。

(tuple:with (pt1 pt2)
  (sqrt (+ (expt (- pt1.x pt2.x) 2)
           (expt (- pt1.y pt2.y) 2))))

   ; new expansion
=> (let ((#:g648 (funcall pt1 :x '#:get-func646))
         (#:g649 (funcall pt2 :x '#:get-func646))
         (#:g650 (funcall pt1 :y '#:get-func646))
         (#:g651 (funcall pt2 :y '#:get-func646)))
     (symbol-macrolet ((pt1.x (tuple::__access #:g648))
                       (pt2.x (tuple::__access #:g649))
                       (pt1.y (tuple::__access #:g650))
                       (pt2.y (tuple::__access #:g651)))
       (sqrt (+ (expt (- pt1.x pt2.x) 2)
                (expt (- pt1.y pt2.y) 2)))))

 

ポイントは、展開結果に含まれる(シンボルとして使用される)#:get-func646 という gensym、そして tuple::__access という関数らしきもの、そしてこれらに関わっている #:g648 〜 #:g651 という gensym だ。これを説明するには、まずエクスポートされない tuple::__access から説明する必要がある。それは以下のようなものだ。

(defun __access (fnc)
  (funcall fnc g-get-tag))

(defun (setf __access) (newval fnc)
  (funcall fnc newval))

 

これで大体わかるだろう。tuple::__access に渡すべきもの、先の展開結果に関して言えば #:g648 〜 #:g651 は、関数(というかクロージャ)なわけだ。そして、それらはクロージャとしての tuple オブジェクトに対して要素名(を示すキーワードシンボル)と #:get-func646 という gensym を渡すことで得ている。これは tuple パッケージの内部で定義されている関数やマクロが共有している gensym で、ソースコード上では g-get-func という名前になっている。おおむね、以下のような感じだ。

(let ((g-get-tag  (gensym "GET-TAG"))
      (g-get-func (gensym "GET-FUNC")))

  (defun __access (fnc)
    (funcall fnc g-get-tag))

  (defun (setf __access) (newval fnc)
    (funcall fnc newval))

  (defun get (obj key)
    (funcall obj key g-get-tag))

  (defun (setf get) (new-val obj key)
    (funcall obj key new-val))
        :
        :

 

‥‥‥ここまできてしまえば、tuple:create の新しい展開結果を見せた方が話が早いだろう(面倒になってきたというのもある)。前々回の展開結果と見比べるとかなり違うことがわかる。

(tuple:create :name "random lisper" :age 40 :gender :male)

=> (let ((#:name   "random lisper")
         (#:age    40)
         (#:gender :male))
     (labels ((#:name-accessor (#:newval)
                (if (eq #:newval '#:get-tag645)
                    #:name
                    (setf #:name #:newval)))
              (#:age-accessor (#:newval)
                (if (eq #:newval '#:get-tag645)
                    #:age
                    (setf #:age #:newval)))
              (#:gender-accessor (#:newval)
                (if (eq #:newval '#:get-tag645)
                    #:gender
                    (setf #:gender #:newval))))
       (lambda (#:key #:newval)
         (if (and (eq #:newval '#:get-tag645) (null #:key))
             (values #:name #:age #:gender)
             (let ((#:accessor (ecase #:key
                                 ((:name)   #'#:name-accessor)
                                 ((:age)    #'#:age-accessor)
                                 ((:gender) #'#:gender-accessor))))
               (if (eq #:newval '#:get-func646)
                   #:accessor
                   (funcall #:accessor #:newval)))))))

 

要するに、それぞれのデータの取得、設定を行うアクセサ関数を用意して、場合によってはそれを外部に返すこともしているわけだ。そして tuple:with マクロは、それを利用して特定のデータを直接読み書きできるクロージャを取得しているというわけ。取得/設定のたびに ecase で「どのメンバか」をディスパッチする必要がなくなる分、速くなったということだ。

ところで、今回の tuple:with マクロの修正は、最初はもっと長かった。symbol-macrolet が展開するのが tuple::__access 関数呼び出しではなく、メンバ毎の局所関数だったのだ(そしてそれももちろん生成していた)。そこから tuple::__access 関数を導入すれば良いことに気付いて現在のかたちになっている。問題は、「もっと長かった」最初の修正が、過去に書いたことがある「クロージャで作成するクラス」のやり方をベースにしていたことで、どうやらそっちも直す感じになりつつある。また寄り道が長くなってきた。

 

コメント

このページにコメントする

 

このページのタグ

Page tag : Common Lisp

 

 


Copyright(C) 2005-2017 project-enigma.
Generated by CL-PREFAB.