#:g1: declare type 対 check-type

Posted 2014-09-09 02:53:00 GMT

 TL;TR 特に結論めいたものはありません。

 SBCLを使っていると、check-typeを書くようなところをdeclare typeで済ませようという気になってしまいます。
declare typeでのチェックは型エラーにすることが保証されている訳ではないので、ちゃんと check-type を使いましょうというのは良く言われることではありますが、SBCLは、safetyを0にしたりしなければ、 check-type 的な動作をしてくれることもありdeclare typeを使ってしまう訳です。

 そんな日々でしたが、Pythonコンパイラの論文であるThe Python Compiler for CMU Common Lispを眺めていたら、2 Type Checkingで、Pythonコンパイラでは、declare typeが強力なので check-type の代わりに積極的に活用していけますよ、的な調子で解説されていて、この中途半端な状況の大元はここだったかと思った最近です。
ちなみに型情報の伝播がdeclare typeの方がcheck-typeより良いようなことが書いてありますが、現在のSBCLでは、check-typeで書いた内容もdeclare typeと同じように伝播するようです(厳密には調べていないので、違っていたら教えてください)。

 そんなcheck-typeとdeclare typeですが、何が違うのかといえばcheck-typeが値を再設定する再起動をするところです。つまり、その分遅くなるのですが、どんなものか調べてみました。

(defun foo (n)
  (declare (integer n)
           (optimize (safety 0) (speed 3) (debug 0)))
  n)

; disassembly for FOO (assembled 9 bytes) ; MOV RDX, RCX ; no-arg-parsing entry point ; MOV RSP, RBP ; CLC ; POP RBP ; RET

(defun foo-ct (n) (declare (optimize (safety 0) (speed 3) (debug 0))) (check-type n integer) n)

; disassembly for FOO-CT (assembled 99 bytes) ; JMP L1 ; no-arg-parsing entry point ; NOP ; NOP ;L0: MOV RDI, RDX ; LEA RBX, [RSP-16] ; SUB RSP, 32 ; MOV RDX, [RIP-131] ; 'N ; MOV RSI, [RIP-130] ; 'INTEGER ; MOV QWORD PTR [RBX-16], 537919511 ; MOV RAX, [RIP-137] ; #<FDEFINITION for SB-KERNEL:CHECK-TYPE-ERROR> ; MOV ECX, 8 ; MOV [RBX], RBP ; MOV RBP, RBX ; CALL QWORD PTR [RAX+9] ; CMOVB RSP, RBX ;L1: LEA EAX, [RDX-15] ; TEST AL, 1 ; JNE L2 ; TEST AL, 15 ; JNE L0 ; CMP BYTE PTR [RDX-15], 17 ; JNE L0 ;L2: MOV RSP, RBP ; CLC ; POP RBP ; RET

(loop :repeat (expt 10 9) :count (foo-ct 1)) ;=> 1000000000 #|------------------------------------------------------------| Evaluation took: 3.817 seconds of real time 3.852000 seconds of total run time (3.852000 user, 0.000000 system) 100.92% CPU 12,564,987,842 processor cycles 32,976 bytes consed

Intel(R) Xeon(R) CPU E3-1230 v3 @ 3.30GHz |------------------------------------------------------------|#

(loop :repeat (expt 10 9) :count (foo 1)) ;=> 1000000000 #|------------------------------------------------------------| Evaluation took: 3.286 seconds of real time 3.300000 seconds of total run time (3.300000 user, 0.000000 system) 100.43% CPU 10,819,408,209 processor cycles 65,632 bytes consed

Intel(R) Xeon(R) CPU E3-1230 v3 @ 3.30GHz |------------------------------------------------------------|#

 大体15%位check-typeの方が遅いようですが、10億回回して、0.5秒の違い。チェックする変数が多ければ差はどんどん開くと思いますが、check-typeでも良いんじゃないかという気もしてきました。
チューニングの時に除去するのも手間でもない気がします(多分)が、policy-condみたいなものを使って、

(defmacro check-type* (place type &optional type-spec)
  `(policy-cond:policy-if (eql 0 cl:safety)
                          (the ,type ,place)
                          (check-type ,place ,type ,type-spec)))

みたいなものを書いて使えば、SAFETY 0の時には再起動しないcheck-typeみたいなものもできるかなと思います。

 ちなみに昔のコードには、両方書いてるような場合も結構ありますが、こんな感じに併用すると

(defun foo-decl&ct (n)
  (declare (optimize (safety 0) (speed 3))
           (integer n))
  (check-type n integer)
  n)

SBCLではdeclareの方が勝つようで、check-typeのコードは消えてなくなります。
ということで、最適化時にcheck-typeを迂回したければ、型宣言を追加するというのもSBCLでは可能ではあります(しかし、deleting unreachable codeの警告が出ます)。

まとめ

 check-typeの方がポータブルでCommon Lisp的には正しいコードだけど、declare typeも捨てがたい、しかし、check-typeで通しても良いんじゃないか、という感じのことを書いてみました。
とはいえ、速度はあまり気にならないところではcheck-typeだなとは思います。

comments powered by Disqus