ラベル Clozure CL の投稿を表示しています。 すべての投稿を表示
ラベル Clozure CL の投稿を表示しています。 すべての投稿を表示

2011-02-01

Clozure CLの最適化について

書きかけ:多分、加筆・訂正します。

Clozure CLは、生成するコードの最適化を、ポリシーオブジェクトというものを使って管理している。対話的なコンパイルに使われるものや、compile-fileに使われるものなど、複数のポリシーオブジェクトが存在するが、デフォルトでは全てのポリシーオブジェクトは同じ設定になっている。

ポリシーオブジェクトは構造体のようなもので、それぞれのスロットには関数が収納されている。関数は、一部の例外を除き、レキシカルな環境を引数として受け取る。イメージとしては、

#'(lambda (env)
    (< (debug-optimize-quantity env) 2))

こんな感じ。コンパイラは、これらの関数が返す真偽値を元にして、最適化を行う。つまり、ポリシーオブジェクトのスロットの数の分だけ、最適化のオプションがある。

具体的な内容は以下の通り。1.6の時点での情報。項目の名前は、ポリシーオブジェクトのスロットにアクセスするマクロの名前で、動作はデフォルトのもの。

policy.allow-tail-recursion-elimination
末尾再帰の最適化をするかどうか。debugが2未満のときに真。
policy.inhibit-register-allocation
詳細不明。debugが3のときに真。
policy.inhibit-event-checking
現状では何にも影響しない。このオプションが存在しないブランチもある。
policy.inline-self-calls
詳細不明。debugが3未満のときに真。
policy.allow-transforms
コンパイラマクロを展開するかどうか。safety、compilation-speed、debugが3未満で、speedが1以上のときに真。
policy.force-boundp-checks
詳細不明。safetyが3のときに真。
policy.allow-constant-substitution
詳細不明。つねに真。
policy.open-code-inline
極端に長いインストラクションをインライン展開するかどうか。speedがspaceより大きいとき真。コストの高いスペシャル変数の参照などがインライン展開される。
policy.inhibit-safety-checking
引数の数、型、束縛の有無などのチェックを省くかどうか。speedが3で、safetyが0のとき真。型のミスマッチがクラッシュを引き起こす可能性がある。
policy.trust-declarations
型宣言が利用されるかどうか。safetyが3未満で、speedがsafety以上のとき真。固定長整数同士の演算では型チェックが省かれ、リストへの操作は安全でないコードが生成される。それ以外の型では型特有のコードが生成されるが、policy.inhibit-safety-checkingが真でなければ、安全なコードが生成される。
policy.declarations-typecheck
型宣言が実行時に行われるかどうか。safetyが3か、speedがsafety未満のとき真。theを実行時の型チェックのコードにコンパイルし、束縛や破壊的変更の際に型チェックを行うコードが挿入される。

以下参考文献。

2010-12-29

文字列操作の性能(1)

Common Lispで文字列を操作する場合に、何種類かある方法のうち、どれが一番効率的か知りたかったので、調べることにした。今回は参照編。

次のコードを使って、シーケンシャルアクセスの時間を測った。処理系はClozure CL 1.6。get-internal-real-timeの単位はミリ秒。

(defmacro with-random-string ((var &optional (length 1000)) &body body)
  `(let ((,var (make-string ,length)))
     (iter (for i index-of-string ,var)
           (setf (aref ,var i) (code-char (+ 32 (random 95)))))
     ,@body))

(defmacro with-null-stream ((var) &body body)
  `(let ((,var (make-two-way-stream (make-concatenated-stream) 
                                    (make-broadcast-stream))))
     (with-open-stream (,var ,var)
       ,@body)))

(defmacro with-benchmark (&body body)
  (with-gensyms (time)
    `(let ((,time (get-internal-real-time)))
       ,@body
       (- (get-internal-real-time) ,time))))

(defparameter *times* 1000)
(defparameter *length* 1024)

(iter (repeat *times*)
      (sum (with-random-string (str *length*)
             (with-null-stream (*standard-output*)
               (with-benchmark
                 (iter (for i index-of-string str)
                       (princ (char str i))))))))

(iter (repeat *times*)
      (sum (with-random-string (str *length*)
             (with-null-stream (*standard-output*)
               (with-benchmark
                 (iter (for i index-of-string str)
                       (princ (aref str i))))))))

(iter (repeat *times*)
      (sum (with-random-string (str *length*)
             (with-null-stream (*standard-output*)
               (with-benchmark
                 (iter (for i index-of-string str)
                       (princ (elt str i))))))))

(iter (repeat *times*)
      (sum (with-random-string (str *length*)
             (with-null-stream (*standard-output*)
               (with-input-from-string (s str)
                 (with-benchmark
                   (iter (for c = (read-char s nil))
                         (while c)
                         (princ c))))))))

結果は、

関数 所要時間(ミリ秒)
char 1093
aref 998
elt 1063
read-char 1189

arefが一番速い。eltは、型に合わせて下位のアクセサを呼ぶと思っていたので、一番速くないのは予想通り。何となくcharはプリミティブなアクセサだと思い込んでいたけど、実際はarefとeltよりも遅い。文字列ストリームが一番遅いのは予想外。予想以上に上位の関数で組み立てられている模様。

実際のコードを読んでみた。まずはchar。

(defun char (string index)
  "Given a string and a non-negative integer index less than the length of
  the string, returns the character object representing the character at
  that position in the string."
  (if (typep string 'simple-string)
    (schar (the simple-string string) index)
    (if (stringp string)
      (multiple-value-bind (data offset) (array-data-and-offset string)
        (schar (the simple-string data) (+ index offset)))
      (report-bad-arg string 'string))))

scharを呼んでいる。scharの定義は、

(defun schar (s i)
  "SCHAR returns the character object at an indexed position in a string
   just as CHAR does, except the string must be a simple-string."
  (let* ((typecode (typecode s)))
    (declare (fixnum typecode))
    (if (= typecode target::subtag-simple-base-string)
      (aref (the simple-string s) i)
      (report-bad-arg s 'simple-string))))

で、結局呼ぶのはaref。Common Lispでは文字列は文字のベクタなので、ベクタに対する処理のarefの方が下位ということなんだろう。

(defun aref (a &lexpr subs)
  "Return the element of the ARRAY specified by the SUBSCRIPTS."
  (let* ((n (%lexpr-count subs)))
    (declare (fixnum n))
    (if (= n 1)
      (%aref1 a (%lexpr-ref subs n 0))
      (if (= n 2)
        (%aref2 a (%lexpr-ref subs n 0) (%lexpr-ref subs n 1))
        (if (= n 3)
          (%aref3 a (%lexpr-ref subs n 0) (%lexpr-ref subs n 1) (%lexpr-ref subs n 2))
          (let* ((typecode (typecode a)))
            (declare (fixnum typecode))
            (if (>= typecode target::min-vector-subtag)
              (%err-disp $XNDIMS a n)
              (if (< typecode target::min-array-subtag)
                (report-bad-arg a 'array)
                ;;  This typecode is Just Right ...
                (progn
                  (unless (= (the fixnum (%svref a target::arrayH.rank-cell)) n)
                    (%err-disp $XNDIMS a n))
                  (let* ((rmi (%array-index a subs n)))
                    (declare (fixnum rmi))
                    (multiple-value-bind (data offset) (%array-header-data-and-offset a)
                      (declare (fixnum offset))
                      (uvref data (the fixnum (+ offset rmi))))))))))))))

arefはあからさまにプリミティブな内容。

(defun elt (sequence idx)
  "Return the element of SEQUENCE specified by INDEX."
  (seq-dispatch
   sequence
   (let* ((cell (nthcdr idx sequence)))
     (if (consp cell)
       (car (the cons cell))
       (if cell
         (report-bad-arg sequence '(satisfies proper-list-p))
         (%err-disp $XACCESSNTH idx sequence))))
       
   (progn
     (unless (and (typep idx 'fixnum) (>= (the fixnum idx) 0))
       (report-bad-arg idx 'unsigned-byte))
     (locally 
       (if (>= idx (length sequence))
         (%err-disp $XACCESSNTH idx sequence)
         (aref sequence idx))))))

eltは、総称関数ではなく、普通の関数。規格上でもそう決まっている。これもarefを呼んでいる。

文字列ストリームは、level-1/l1-streams.lispに定義がある。read-charしたとき、最終的に呼ばれるのは、おそらくこれ。

(defun string-input-stream-ioblock-read-char (ioblock)
  (let* ((string (string-stream-ioblock-string ioblock))
         (idx (string-input-stream-ioblock-index ioblock))
         (end (string-input-stream-ioblock-end ioblock)))
    (declare (fixnum idx end)
             (simple-string string))
    (if (< idx end)
      (progn (setf (string-input-stream-ioblock-index ioblock)
                   (the fixnum (1+ idx)))
             (schar string idx))
      :eof)))

scharを呼んでいるので、最終的に呼ばれるのはaref。

というわけで、実際の定義も計測結果を裏付ける内容だった。定義を見る限り、ランダムアクセスでも順位は変わらない。文字列ストリームだけ、file-positionで位置を指定する分、他と差が開くかもしれない。

結論としては、Clozure CL 1.6においては、文字列の要素を参照する場合、arefが一番速い。

情報を頂いたので追記。SBCLでは、最適化を掛けると、charやeltがarefに展開されるようで、ディスアセンブルした結果も一致するとのこと。良いなあ。

2010-11-03

Clozure CL 1.6 RC1

Clozure CLの1.6 RC1が出てた。

ARMアーキテクチャ対応が一番の目玉だろうけど、さり気にTicket #696Ticket #731が修正されてるのも大きい。これで、1.4以降から続いていたWindowsでの問題もひと段落して、最近のLinuxでsegmentation faultになる問題も解決された。両環境で、trunk以外まともに使えなかった状況が、ようやく改善された形。

あとは、ASDFがASDF2になったり、ネットワーク越しにloadできるようになったり、色々。