maphash-to-list

common lispではハッシュが使い難い(と思う)
その理由の一つは、リストのようにマッピング関数がない*1せいだと思うので、そのための関数を定義。
ユーティリティ関数っぽくいろいろ装飾。
※ 宣言はsbcl(1.0.37)用に特化

(declaim (inline maphash-to-list))  ; sbclのmaphash関数にはinline宣言されているようなので、それに合わせる
(defun maphash-to-list (function-designator hash-table &aux acc)
"For each entry in HASH-TABLE, call the designated two-argument function on
 the key and value of the entry. Return list of the function return values."

  ;; 引数の型チェック等を行って欲しいため、safetyやdebugをここでは0にしない
  ;;
  ;; (/= safety 0)なら、関数呼び出し時に型チェックが行われる
  ;; - http://www.sbcl.org/manual/#Declarations-as-Assertions
  ;; 
  ;; (>= debug 1)なら、関数の引数情報が保持される => (describe ...)の結果が親切
  ;; - http://www.sbcl.org/manual/#Debugger-Policy-Control
  (declare (optimize (safety 1) (debug 1) (speed 3))
           (hash-table hash-table)
           ((or symbol function) function-designator))
  (locally
   ;; この時点では引数の型が正しいことが確定しているので、debug及びsafetyを0に設定
   (declare (optimize (debug 0) (safety 0)))
   (let ((fn (if (typep function-designator 'function)
                 function-designator
               (symbol-function function-designator))))
     (maphash
      (lambda (k v)
        (push (funcall fn k v) acc))
      hash-table))
   acc))

;;;;;;;
;;; 例
> (let ((hash (make-hash-table)))
    (setf (gethash :key hash) :value
          (gethash 1 hash) 2
          (gethash "a" hash) "b")
  
    ;; キーだけを集める
    (maphash-to-list (lambda (k v) k) hash))
--> ("a" 1 :KEY)

比較のために、型チェックを全く行わない版も定義。

(declaim (inline maphash-to-list-fast))
(defun maphash-to-list-fast (function-designator hash-table &aux acc)
  (declare (optimize (safety 0) (debug 0) (speed 3))
           (hash-table hash-table)
           (function function-designator))
  (maphash
   (lambda (k v)
     (push (funcall function-designator k v) acc))
   hash-table)
  acc)

比較。

;;;;;;;;;;;;;;;;;;
;;; 型チェック確認
> (maphash-to-list 1 1)
debugger invoked on a TYPE-ERROR in thread #<THREAD "initial thread" RUNNING
                                             {AA087C1}>:
  The value 1 is not of type (OR SYMBOL FUNCTION).  ; 型が違う

> (maphash-to-list-fast 1 1)
CORRUPTION WARNING in SBCL pid 9089(tid 3085166256):
Memory fault at 3b (pc=0xb61914a, sp=0xb7907b88)
The integrity of this image is possibly compromised.
Continuing with fingers crossed.

debugger invoked on a SB-SYS:MEMORY-FAULT-ERROR in thread #<THREAD
                                                            "initial thread" RUNNING
                                                            {AA087C1}>:
  Unhandled memory fault at #x3B.  ; Unhandled memory?

;;;;;;;;;;;;;
;;;; 処理速度
;; 要素数1000のハッシュを作成
> (defparameter *hash*
    (let ((hash (make-hash-table)))
      (loop FOR i FROM 0 BELOW 1000 DO
        (setf (gethash i hash) (random 10000000)))
      hash))
--> *HASH*

;; maphash関数の場合
> (time
   (dotimes (i 100000 'done)
     (declare (optimize (speed 3) (debug 0) (safety 0)))
     (let (acc)
       (maphash
         (lambda (k v)
           (declare (fixnum k v))
           (push (+ k v) acc))
         *hash*))))
Evaluation took:
  0.845 seconds of real time
  0.844052 seconds of total run time (0.844052 user, 0.000000 system)
  [ Run times consist of 0.060 seconds GC time, and 0.785 seconds non-GC time. ]
  99.88% CPU
  2,680,836,701 processor cycles
  800,000,936 bytes consed
--> DONE

;; maphash-to-list関数の場合
> (time
   (dotimes (i 100000 'done)
     (declare (optimize (speed 3) (debug 0) (safety 0)))
     (maphash-to-list 
      (lambda (k v)
        (declare (fixnum k v))
        (+ k v))
      *hash*)))
Evaluation took:
  0.849 seconds of real time
  0.848054 seconds of total run time (0.844053 user, 0.004001 system)
  [ Run times consist of 0.072 seconds GC time, and 0.777 seconds non-GC time. ]
  99.88% CPU
  2,693,672,940 processor cycles
  800,002,736 bytes consed
--> DONE

;; maphash-to-list-fast関数の場合
> (time
   (dotimes (i 100000 'done)
     (declare (optimize (speed 3) (debug 0) (safety 0)))
     (maphash-to-list-fast
      (lambda (k v)
        (declare (fixnum k v))
        (+ k v))
      *hash*)))
  Evaluation took:
  0.848 seconds of real time
  0.844052 seconds of total run time (0.836052 user, 0.008000 system)
  [ Run times consist of 0.064 seconds GC time, and 0.781 seconds non-GC time. ]
  99.53% CPU
  2,687,459,342 processor cycles
  800,005,176 bytes consed
--> DONE

どれもほとんど速度は変わらない。
今回のような関数の場合は、入り口部分で型チェックをしたからといってそこがボトルネックとなることもないので(inline化されているならなおさら)、安全性を高め文書化するために、適切な宣言を行った方が良い。
結構サボリがちだけど、今後ユーティリティ関数やライブラリを書く時には気をつけないと...。

*1:loopマクロを使えば出来ないことはないけど、これはこれで使い難いように思う。ハッシュのキーと値の両方を使いたい時は特に。

ftype型宣言(sbcl) : 戻り値の型指定

前回と重複するところがあるが、ftype型宣言の有用な使い方について書いておく。

関数の戻り値の型指定

ftype型宣言を使うと、関数の戻り値の型を指定することができる。
これが何故有用かと云うと、sbclは関数の戻り値の型が指定されている場合、それに基づいて型推論を行ってくれるので、(前回の例でも挙げたように)呼び出し側での型宣言が不要となる。


以下、例。
まずは、ftype型宣言がない場合。

;; バイナリファイル読み込み関数
;; 戻り値の型指定無し
(defun read-binary-file (path)
  (with-open-file (in path :element-type '(unsigned-byte 8))
    (let ((as (make-array (file-length in) :element-type '(unsigned-byte 8))))
      (read-sequence as in)
      as)))

;; read-binary-fileの呼び出し側
(defun fn (path)
  (declare (optimize (speed 3))
           (sb-ext:unmuffle-conditions sb-ext:compiler-note))
  (loop FOR c ACROSS (read-binary-file path) COLLECT c))
;;;;
;; ループ対象の配列の型(ランク)が不明なため、最適化が行えないと云われる
;
; in: LAMBDA NIL
;     (LOOP FOR C ACROSS (READ-BINARY-FIL PATH)
;           COLLECT C)
; --> BLOCK LET SB-LOOP::WITH-LOOP-LIST-COLLECTION-HEAD LET* SB-LOOP::LOOP-BODY 
; --> TAGBODY SB-LOOP::LOOP-REALLY-DESETQ SETQ THE AREF 
; ==>
;   (SB-KERNEL:HAIRY-DATA-VECTOR-REF/CHECK-BOUNDS ARRAY SB-INT:INDEX)
; 
; note: unable to
;   optimize
; because:
;   Upgraded element type of array is not known at compile time.
; 
; compilation unit finished
;   printed 1 note
--> FN

;; 型を明示する
(defun fn (path)
  (declare (optimize (speed 3))
           (sb-ext:unmuffle-conditions sb-ext:compiler-note))
  (loop FOR c ACROSS (the (simple-array (unsigned-byte 8)) 
                          (read-binary-file path)) COLLECT c))
--> FN  ; 警告はでない


次が、ftype型宣言をつけた場合。

;; 分かりやすさのために、型の別名を定義する
(deftype simple-octets () '(simple-array (unsigned-byte 8)))

;; バイナリファイル読み込み関数
;; simple-octets型の値を返すことを宣言する
(declaim (ftype (function (t) simple-octets) read-binary-file))  ; XXX: 関数の引数の型は、本当は(or stream string pathname)とかが適切
(defun read-binary-file (path)
  (with-open-file (in path :element-type '(unsigned-byte 8))
    (let ((as (make-array (file-length in) :element-type '(unsigned-byte 8))))
      (read-sequence as in)
      as)))

;; read-binary-fileの呼び出し側
(defun fn (path)
  (declare (optimize (speed 3))
           (sb-ext:unmuffle-conditions sb-ext:compiler-note))
  (loop FOR c ACROSS (read-binary-fil path) COLLECT c))
--> FN  ; 呼び出し側で、型を明示しなくても警告はでない


また、ftypeを使って関数の型を宣言しておいた場合、呼び出し側の関数で特にoptimize宣言などを行なっていない場合でも、コンパイラが勝手に型を推論して最適化を行ってくれる。
※ ただし、ftypeを使わなくても、コンパイラが(関数の定義から)戻り値の型を推論してくれる場合が結構ある*1

;; まだread-binary-fileの型宣言は行っていない
> (defun fn (path)
    (loop FOR c ACROSS (read-binary-fil path) COLLECT c))
--> FN  ; (optimize ...)及び(unmuffle ...)宣言をつけていないので、警告はでない。最適化も行わない。

;; bashコマンドの読み込み
> (time (progn (fn "/bin/bash") 'done))
Evaluation took:
  0.023 seconds of real time  ; 0.023s
  0.024002 seconds of total run time (0.024002 user, 0.000000 system)
  104.35% CPU
  63,412,396 processor cycles
  6,321,280 bytes consed
--> DONE

;;
;; 型宣言を付与
> (declaim (ftype (function (t) simple-octets) read-binary-file))

;; 中身は変えずに再定義
> (defun fn (path)
    (loop FOR c ACROSS (read-binary-fil path) COLLECT c))
--> FN

;; bashコマンドの読み込み
> (time (progn (fn "/bin/bash") 'done))
Evaluation took:
  0.015 seconds of real time  ; 0.015s  ->  1.5倍くらい速くなる
  0.016001 seconds of total run time (0.016001 user, 0.000000 system)
  106.67% CPU
  41,930,544 processor cycles
  6,321,528 bytes consed
--> DONE

このため、ユーティリティ関数などで戻り値の型があらかじめ決まっている場合は、ftypeを使って型を宣言しておくのが良いのではないかと思う*2 *3
そうすれば、使用側は特に意識せずとも、型情報がついた関数を呼び出すだけで可能な最適化をコンパイラが行ってくれるし、明示的な最適化を行いたい場合も、自分で書く宣言の量を減らすことができる。

*1:特定の関数に十分な型情報がついているかどうかは、describe関数を使って確認できる

*2:実際には、上でも書いたようにftypeを使わなくても、既に十分な型情報がコンパイラにより付与されている場合がある(むしろそっちの方が多い。本文では(declaim (ftype ...) )の必要性を強く主張しすぎ )ので、一度試してみて必要なものにだけ宣言を行えば良いと思う。

*3:関数を適切に定義しておけば、呼び出し側でその型情報が暗黙の内に利用される、ということの方が重要。(declaim (ftype ...) )は、そのための方法の一つ。

ftype型宣言(sbcl)

ftype型宣言。
あまり使われているのを見ない気がするが、これを使うと「型宣言なしでは遅いけど、宣言をつけるとコードが汚くなる」というような問題を解決できる時があるので、少し書いておく。

以下はsbcl(1.0.34)での挙動。

準備

;; 処理速度を最優先
(declaim (optimize (speed 3) (debug 0) (safety 0) (compilation-speed 0)))

;; unsigned int型
(deftype uint (max) `(integer 0 ,max))

;; 計時関数
(defun test ()
  (time
    (dotimes (i 100000)
      (dotimes (j 10)
        (dotimes (k 10)
          (fn2 j k))))))  ; fn2関数は後で定義する

型宣言なし

型宣言なしの関数定義。遅い。

> (defun fn (a b)
    (* a b))
--> FN

> (defun fn2 (a b)
    (+ (fn a b) (fn b a)))
--> FN2

> (test)
Evaluation took:
  0.165 seconds of real time
  0.164010 seconds of total run time (0.164010 user, 0.000000 system)
  99.39% CPU
  523,636,114 processor cycles
  0 bytes consed
--> NIL

型宣言あり

型宣言あり。速いけど、関数定義が(宣言なしのものに比べ)汚い。

> (defun fn (a b)
    (declare ((uint 10) a b))
    (* a b))
--> FN

> (defun fn2 (a b)
    (+ (the (uint 100) (fn a b)) (the (uint 100) (fn b a))))
--> FN2

> (test)
Evaluation took:
  0.093 seconds of real time
  0.096006 seconds of total run time (0.096006 user, 0.000000 system)
  103.23% CPU
  295,587,465 processor cycles
  0 bytes consed
--> NIL

型宣言あり(ftype版)

ftype版。関数定義は型宣言なしのものと変わらない。

;; fn関数の型を宣言
(declaim (ftype (function ((uint 10) (uint 10)) (uint 100)) fn))

> (defun fn (a b)
    (* a b))
--> FN

> (defun fn2 (a b)
    (+ (fn a b) (fn b a)))  ; 上の宣言でfn関数が(uint 100)を返すことが分かっているので、(the ...)は不要
--> FN2

> (test)
Evaluation took:
  0.094 seconds of real time
  0.096006 seconds of total run time (0.096006 user, 0.000000 system)
  102.13% CPU
  297,934,421 processor cycles
  0 bytes consed
--> NIL

上のような簡潔な関数だと、どちらでもあまり変わらないような気もするが、少し複雑な関数定義の場合は、特に(the ...)の有る無しで、結構見やすさが変わってくるように思う。
たまに便利。

ただ、上の例でもそうだが、(declaim (ftype ...))を使って関数定義の外で宣言をした場合、定義内で宣言するのに比べて、生成されるコードが若干だが非効率になる傾向があるようなので、(最適化が必要なケースでは)結局あまり使えないかもしれない...。

C++とcommon lispの実行速度比較(素数判定)

今日たまたま見つけた『(Sather を試そう) 1. Sather vs C++: 実行速度の比較』*1というページに触発され、lisp(sbcl-1.0.28)でも同様の比較を行ってみた。


C++ソースコードは、上記ページのものと同様だが、g++のオプションには'-O3'を渡している。
実行結果は以下の通り。

> time ./prime_cpp 10000000 > /dev/null
real	0m1.032s
user	0m0.992s
sys	0m0.004s


対するlispのコード。
※ 変数名などは、自分の好みに合わせて変えているが、アルゴリズム的にはC++のものと同様(のはず)。

;;;; 宣言などのため、C++のソースより長くなってしまっている
(declaim (optimize (debug 0) (safety 0) (speed 3) (compilation-speed 0))
         (inline primep))

(defun primep (n primes len)
  ;; inline化されるので、ここの宣言は(今回のケースでは)無くても大丈夫
  (loop FOR i FROM 0 BELOW len
        FOR p fixnum = (svref primes i) DO
    (when (> (the fixnum (* p p)) n) (return t))
    (when (zerop (mod n p))          (return nil))))

(defun nprime (limit)
  (format t "#Limit, N of primes~%0 0~%")
  (let ((primes (make-array (truncate (/ limit 2))))
        (len 0)  ; primesの長さは、プログラムで管理する
        (interval (- 500 2)))
    (declare (fixnum limit len interval)
             (simple-vector primes))
    (macrolet ((post-incf (n)    `(prog1 ,n (incf ,n)))
               (push-back (elem) `(setf (aref primes (post-incf len)) ,elem)))
      (push-back 2)
      (push-back 3)
      (loop FOR n FROM 5 TO limit BY 2 DO
        (when (primep n primes len) 
          (push-back n))
        (when (zerop (decf interval))
          (format t "~D ~D~%" n len)
          (setf interval 500))))))


実行結果。

>(let ((*standard-output* (make-broadcast-stream)))
   (time (nprime 10000000)))
Evaluation took:
  1.152 seconds of real time
  1.156072 seconds of total run time (1.156072 user, 0.000000 system)
  100.35% CPU
  3,655,271,262 processor cycles
  20,000,008 bytes consed

なかなか、悪くない結果だ。
lisp(sbcl)版は、C++版の1.15倍程度に収まっている*2

おまけ

宣言を外したり、primesの実装を変更した場合の処理速度の違いを以下にまとめる。

番号primes実装/宣言の有無実行時間(秒)
0simple-vector/宣言有(上の実装)1.152
1simple-vector/宣言無6.632
2fill-pointer-vector/宣言有9.576
3fill-pointer-vector/宣言無8.936
4fill-pointer-vector/primepのみ宣言無*312.943
5fill-pointer-adjustable-vector/宣言有*49.605
6list/宣言有1.167
7list/宣言無5.499
listでの実装(queueを使用)が意外に速く、fill-pointer付きvectorが遅かった。
多分、上記プログラムのボトルネックの一つは、primep関数内でのsequenceの各要素のイテレートなのだろうが、そこでのlistに対するcarとcdrが速く、fill-pointer-vectorに対するarefが(相対的に)遅いために、この様な結果になったのだと思う。


また、fill-pointer-vectorでは、何故か最適化宣言が有る場合の方が、無い場合よりも遅くなっている。
例えば、上の表の2番(宣言有)と3番(宣言無)の実行時間はあまり変わらないが、これは2番の場合、宣言によってfill-pointer-vector周りの処理で遅くなってprimep関数内での数値計算が速くなっているからだろう(両者が相殺)。
そのため、4番のようにprimep関数内の宣言だけを外した場合は、明らかに実行時間が延びている。


あと、adjustableは付けても付けなくても、それほど大きな影響がない感じだ。そもそも、今回は挿入回数が少なかったので、そのせいもあるだろう。

おまけ2

listでの実装が予想外に速かったので、このソースコードも載せておく。

(declaim (optimize (debug 0) (safety 0) (speed 3) (compilation-speed 0))
         (inline primep))

;; listによるqueue実装(必要な関数のみ)
(declaim (inline make-que push-que que-list))
(defun make-que ()    (let ((q (list nil))) (setf (car q) q)))
(defun push-que (x q) (setf (car q) (setf (cdar q) (list x))))
(defun que-list (q)   (cdr q))


(defun primep (n primes)
  (dolist (p (que-list primes))
    (declare (fixnum p))
    (when (> (the fixnum (* p p)) n) (return t))
    (when (zerop (mod n p))          (return nil))))

(defun nprime (limit)
  (format t "#Limit, N of primes~%0 0~%")
  (let ((primes (make-que))
        (len 2) ;; listのlengthはコストが高いので、長さは別に管理する
        (interval (- 500 2)))
    (declare (fixnum limit interval len))
    (push-que 2 primes)
    (push-que 3 primes)
    (loop FOR n FROM 5 TO limit BY 2 DO
      (when (primep n primes)
        (incf len)
        (push-que n primes))
      (when (zerop (decf interval))
        (format t "~D ~D~%" n len)
        (setf interval 500)))))

*1:Nまでの素数を探すプログラムをSather(?)とC++で実装・比較している

*2:ちなみに、後日、別のマシンで試してみたところ、そこでは1.5倍程度の差がでた

*3:primep関数定義内でのfixnum宣言を除去

*4:初期サイズは64

コンパイラノート表示

最近のバージョンのSBCL*1では、処理速度を最優先にする宣言を行って関数をコンパイルした場合でも、デフォルトではコンパイラノート*2が表示されないことがある*3


そのため、最適化を確実に行いたい場合は、コンパイラノートを出力するように明示的に宣言する必要がある。

(declare (optimize (debug 0) (safety 0) (speed 3) (compilation-speed 0))
         ;; ↓これが必要
         (sb-ext:unmuffle-conditions sb-ext:compiler-note)) 

また、これはどうやらdeclaimで宣言した場合は無効で、declareを使う必要があるようだ。

*1:今回試したのは1.0.28

*2:最適化を行うために修正が必要な箇所などを教えてくれる

*3:多分compile-file関数でコンパイルを行う場合は、デフォルトで表示され、REPL上でコンパイル(関数定義)する場合は表示されない

型分岐最適化

sbcl(1.0.28)は、適切に型宣言を行っておけば、冗長な型判定を実行コードから除外してくれるようだ。

例えば次のようなマクロを定義する。

(defmacro get-type (obj)
  `(typecase ,obj
     (list   :list)
     (fixnum :fixnum)
     (vector :vector)
     (t      :others)))

; ↓のような定義でも可
;(defmacro get-type (obj)
;  `(cond ((typep ,obj 'list)   :list)
;         ((typep ,obj 'fixnum) :fixnum)
;         ((typep ,obj 'vector) :vector)
;         (t                    :others)))

これは単純にobjに対応する型名を返すだけものだが、これをdisassembleすると次のような結果となる。

(disassemble
  (lambda (obj)
    (declare (optimize (safety 0)))  ; disassembleの出力を見やすくするために(safety 0)を宣言
    (get-type obj)))

;disassembly for (LAMBDA (OBJ))
; 0B197ECC:       8BC1             MOV EAX, ECX               ; no-arg-parsing entry point
;      ECE:       2407             AND AL, 7
;      ED0:       3C03             CMP AL, 3
;      ED2:       750B             JNE L1
;      ED4:       8B15987E190B     MOV EDX, [#xB197E98]       ; :LIST
;      EDA: L0:   8BE5             MOV ESP, EBP
; ... 省略 ...
;      EFB:       3CF6             CMP AL, 246
;      EFD:       7608             JBE L3
;      EFF: L2:   8B159C7E190B     MOV EDX, [#xB197E9C]       ; :OTHERS
;      F05:       EBD3             JMP L0
;      F07: L3:   8B15A07E190B     MOV EDX, [#xB197EA0]       ; :VECTOR
;      F0D:       EBCB             JMP L0
;      F0F: L4:   8B15A47E190B     MOV EDX, [#xB197EA4]       ; :FIXNUM
;      F15:       EBC3             JMP L0

出力コード(のコメント)の中に:LISTや:VECTORなどが含まれていることが見てとれる。
「objの型を検査して、分岐し、結果(keyword)を返す」というようなことをやっているのだろう。
ほぼマクロの定義通りのコードといった感じだ。


今度は、同じものに型宣言を加えて、再度disassembleを行う。

(disassemble
  (lambda (obj)
    (declare (optimize (safety 0))
             (list obj))           ; objはlist型
    (get-type obj)))

; disassembly for (LAMBDA (OBJ))
; 0B1C9562:       8B1538951C0B     MOV EDX, [#xB1C9538]       ; :LIST
                                                              ; no-arg-parsing entry point
;        8:       8BE5             MOV ESP, EBP
;        A:       F8               CLC
;        B:       5D               POP EBP
;        C:       C3               RET

型宣言(この場合はlist型)を加えると、出力させるコードは明らかに短くなっており、宣言された型以外に対応する節が完全に除外されていることが分かる。
ちゃんと型宣言をみて、最適化が行われているようだ。


この型宣言による最適化があると、ある種の汎用的な関数が(気分的に)書きやすくなるので嬉しい。
例えば、次のコード。

;; 標準のeltと似た動作をする関数をinline宣言付きで定義
(declaim (inline %elt))
(defun %elt (object index)
  (typecase object
    (list   (nth index object))
    (string (char object index))
    (array  (aref object index))))

;;;;;;;;
;;;; 型宣言付きで%eltを使う場合と、charを直接使う場合を比較
(disassemble
 (lambda (x i) 
   (declare (optimize (safety 0)) (string x))
   (%elt x i)))

(disassemble 
 (lambda (x i) 
   (declare (optimize (safety 0)))
   (char x i)))

;; どちらも同じ結果(↓)を出力する
; disassembly for (LAMBDA (X I))
; 0B1E7077:       8B7DFC           MOV EDI, [EBP-4]           ; no-arg-parsing entry point
;       7A:       8BD6             MOV EDX, ESI
;       7C:       8B0548701E0B     MOV EAX, [#xB1E7048]       ; #<FDEFINITION object for SB-KERNEL:HAIRY-DATA-VECTOR-REF>
;       82:       B908000000       MOV ECX, 8
;       87:       FF7504           PUSH DWORD PTR [EBP+4]
;       8A:       FF6005           JMP DWORD PTR [EAX+5]
NIL

%eltは複数の型に対応する汎用的なアクセサだが、型宣言を付与することで、より特殊な関数を直接呼び出すのと同じ効果が得られている。※ applyを適用した場合は異なる?
そのため、型分岐のオーバヘッドを嫌って、わざわざ個別に(elt-char、elt-vectorelt-xxx)関数を定義し使用する必要性が(完全にではないにしても、ある程度は)なくなるように思う。


残念なのは、この最適化は処理系(この場合は、sbcl)依存だということだ。
試しにclispで同様のコードを実行してみたところ、上記最適化は行われていなかった。※ ただし、僕はclispをほとんど使わないので分からないが、何かを上手くやれば最適化されるかもしれない