2014-06-17-01-ファンクタの0x11対応 - 3 - project-enigma

2014-06-17-01-ファンクタの0x11対応 - 3

>> Site top >> weblog >> 月別アーカイブ >> 2014年06月のlog >> 2014-06-17-01-ファンクタの0x11対応 - 3

最終更新日付:2014/06/17 23:50:00


ファンクタの0x11対応 - 3

2014 年 06 月 17 日

前回、「都度レキシカルクロージャを作って返すのは遅い」と書いた。まぁ容易に想像できることではあるのだが、では、どれくらい‥‥‥? 今回はそういう話。

 

まず、以下のようなカウンタクラスを考える。

(defclass counter1 (stl:functor)
  ((val :type     :integer
        :initform 0
        :initarg  :value
        :accessor counter1-value)))

(labels ((__counter1-ctor (n)
           (make-instance 'counter1 :value n)))
  (stl:constructor/defun counter1 ()  (__counter1-ctor 0))
  (stl:constructor/defun counter1 (n) (__counter1-ctor n))
  (defmethod stl:clone ((func counter1))
     (__counter1-ctor (counter1-value func))))

(defmethod stl:functor-function ((func counter1))
  (lambda ()
    (prog1
        (counter1-value func)
      (incf (counter1-value func)))))

 

ポイントは、functor-function 内部でレキシカルクロージャを作成して返していることだ。これが効率が悪いような気がしているわけだ。そこで、次のような別の実装を考える。これは、functor-function が返すべきクロージャを内部に腹持ちする。

(defclass counter2 (stl:functor)
  ((val :type     :integer
        :initform 0
        :initarg  :value
        :accessor counter2-value)
   (fnc :type     :function
        :initform nil
        :initarg  :closure
        :accessor counter2-closure)))

(labels ((__counter2-ctor (n)
           (let ((obj (make-instance 'counter2 :value n)))
             (setf (counter2-closure obj)
                   (lambda ()
                     (prog1
                         (counter2-value obj)
                       (incf (counter2-value obj)))))
             obj)))
  (stl:constructor/defun counter2 ()  (__counter2-ctor 0))
  (stl:constructor/defun counter2 (n) (__counter2-ctor n))
  (defmethod stl:clone ((func counter2))
    (__counter2-ctor (counter2-value func))))

(defmethod stl:functor-function ((func counter2))
  (counter2-closure func))

 

で、これら2つのファンクタを、それぞれ2種類の方法でコールしてみる。ループ内部で毎回 stl:functor-invoke する方法と、先にクロージャを取得してループ内部ではひたすらそれをコールする方法。結果は以下のとおり。

(let ((gen (stl:new :counter1 () :cl-user)))
  (time (dotimes (i 10000000)
          (stl:functor-invoke gen))))

;Evaluation took:
;  0.608 seconds of real time
;  0.608404 seconds of total run time (0.608404 user, 0.000000 system)
;  100.00% CPU
;  1,606,784,320 processor cycles
;  160,022,480 bytes consed
  

(let* ((gen (stl:new :counter1 () :cl-user))
       (fnc (stl:functor-function gen)))
  (time (dotimes (i 10000000)
          (funcall fnc))))

;Evaluation took:
;  0.374 seconds of real time
;  0.374402 seconds of total run time (0.374402 user, 0.000000 system)
;  100.00% CPU
;  1,006,572,857 processor cycles
;  0 bytes consed
  

(let ((gen (stl:new :counter2 () :cl-user)))
  (time (dotimes (i 10000000)
          (stl:functor-invoke gen))))

;Evaluation took:
;  0.608 seconds of real time
;  0.608404 seconds of total run time (0.608404 user, 0.000000 system)
;  100.00% CPU
;  1,581,072,650 processor cycles
;  0 bytes consed
  

(let* ((gen (stl:new :counter2 () :cl-user))
       (fnc (stl:functor-function gen)))
  (time (dotimes (i 10000000)
          (funcall fnc))))

;Evaluation took:
;  0.390 seconds of real time
;  0.390003 seconds of total run time (0.390003 user, 0.000000 system)
;  100.00% CPU
;  1,025,893,373 processor cycles
;  0 bytes consed

 

毎回レキシカルクロージャを作成して返す場合、メモリ消費が大きくなることがわかる。レキシカルコンテキストの保存には時間的にも空間的にもコストがかかるのだろう。クロージャを腹持ちするか、もしくは呼び出し元でキャッシュすればメモリ消費は抑えられるようだ。そして、やはり functor-invoke 呼び出しのコスト ── つまり総称関数呼び出しのコスト ── もまた無視できないことがわかる。

そんなわけで、パフォーマンスが問題になる(可能性がある)ファンクタでは、functor-function が返すべきクロージャはファンクタ内部で事前に用意しておくべきこと、そして呼び出し元では functor-invoke を繰り返しコールするのではなく、functor-function でクロージャを一度だけ取得し、それを利用すると良い‥‥‥と。

これがなんだか美しくないというのは自分でも承知している。ひょっとしたら、レストパラメータでファンクタ全てをカバーすることはそれほどコスト高でもないかもしれない。しかし、まぁとりあえずはこれでいくつもり。

そして、この話題はまだ続くんだな。未だに自分を悩ませている、bind というやつだ。あれをきちんと、そして効率良く実装する方法が、自分にはまだ見出せないでいる。次回はそれについて書こう。

 

コメント

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

 

このページのタグ

Page tag : Common Lisp

Page tag : STLとその移植

 

 


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