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

ftype型宣言(sbcl)

common lisp sbcl optimize

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