2019年7月6日土曜日

SBCLのeq

SBCLのeq

SBCLのeqの挙動が気になったので。

内部でポインタに変換されないオブジェクトは、値が同一ならeqで区別できない:

CL-USER> (eq 1f0 1f0)
T
CL-USER> (eq 1d0 1d0)
NIL
CL-USER> (eq 1 1)
T
CL-USER> (eq (1+ most-positive-fixnum) (1+ most-positive-fixnum))
NIL
CL-USER> (eq #\あ #\あ)
T

上の例で言うと、fixnumsingle-floatcharacterはボクシングされない。double-floatはボクシングされるのでeqで区別される。

ややこしいのはfixnumの範囲を超える64ビット整数の扱いで、REPLでは上のように偽になるが、コンパイラがうまくやれば(unsigned-byte 64)(signed-byte 64)はボックス化なしで扱われるため、結果が変わったりする:

(defun number-eq (x)
  (eq (+ x 1) (+ x 1)))

;; NUMBER型でコンパイルされているので、
;; FIXNUMの範囲を超えるとボクシングされる
(number-eq most-positive-fixnum)
;; => NIL

;; FIXNUMはボクシングされない
(number-eq 0)
;; => T

;; INTEGER型でもボクシングされる
(defun integer-eq (x)
  (declare (integer x))
  (eq (+ x 1) (+ x 1)))

(integer-eq most-positive-fixnum)
;; => NIL

;; FIXNUMを宣言すれば(+ X 1)が(SIGNED-BYTE 64)の範囲にあると推論されて、
;; ボクシングなしのコードになる
(defun int64-eq (x)
  (declare (fixnum x))
  (eq (+ x 1) (+ x 1)))

(int64-eq most-positive-fixnum)
;; => T

また、定数畳み込みも影響することがある:

(defun constant-eq ()
  (eq (1+ most-positive-fixnum) (1+ most-positive-fixnum)))
  
(constant-eq)
;; => T

(disassemble #'constant-eq)
;; そもそも返り値がTに畳み込まれている。
; disassembly for CONSTANT-EQ
; Size: 28 bytes. Origin: #x10026915C3
; C3:       498B4560         MOV RAX, [R13+96]                ; no-arg-parsing entry point
                                                              ; thread.binding-stack-pointer
; C7:       488945F8         MOV [RBP-8], RAX
; CB:       840425F8FF1020   TEST AL, [#x2010FFF8]            ; safepoint
; D2:       BA4F001120       MOV EDX, #x2011004F              ; T
; D7:       488BE5           MOV RSP, RBP
; DA:       F8               CLC
; DB:       5D               POP RBP
; DC:       C3               RET
; DD:       CC0F             BREAK 15                         ; Invalid argument count trap

;; notinlineでコード変換を禁止するとボクシングされる
(defun not-constant-eq ()
  (declare (notinline 1+))
  (eq (1+ most-positive-fixnum) (1+ most-positive-fixnum)))

(not-constant-eq)
;; => NIL

double-float型についてはeqをトランスレートするVOPがないので、基本的にはどうやってもeqにならない:

(defun double-float-eq (x)
  (declare (double-float x))
  (eq (+ x 0d0) (+ x 0d0)))

(double-float-eq 1d0)
;; => NIL

;; ゼロ加算を消去するトランスフォーマーは(デフォルトでは無効だが)存在するので、
;; このケースに限ってTにすることは可能。以下は(eq x x)と同等になる。
(defun constant-double-float-eq (x)
  (declare (double-float x)
           (optimize (sb-c::float-accuracy 0)))
  (eq (+ x 0d0) (+ x 0d0)))

(constant-double-float-eq 1d0)
;; => T

また、32ビット環境だといろいろ事情が異なると思われる。

まとめ

SBCLでは、fixnumに限ればeqeqlの動作はたぶん同じで、それ以外のeqの挙動は複雑、という感じ。