Igo : sbcl-1.0.28, sbcl-1.0.37

以前、計時を行った条件に合わせてcommon lisp版のIgo(0.2.3)sbclのバージョン1.0.28で動かしてみたところ、処理を終えるまでに92.712秒かかった。
sbclのバージョンを1.0.37に替え、それ以外は同様の条件で試してみたところ、こちらは33.962秒で終了。
3倍近く処理時間が違う。

何故こんなに差がでるのか簡単に調べてみたところ、原因の一つはwith-slotsマクロの最適化にあるらしいことが分かった。
以下、例。

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; code-stream.lispというファイル内のunread関数のdisassemble結果比較

;; 関数定義
;; with-slotsマクロを使っているということだけが重要
(defun unread (code-stream)
  (declare (code-stream code-stream))
  (with-slots (position surrogate?) code-stream
    (if surrogate? 
        (setf surrogate? nil)
      (decf position))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; sbcl-1.0.37でのdisassemble結果
; disassembly for IGO.CODE-STREAM:UNREAD
; 0AB75B42:       8B420F           MOV EAX, [EDX+15]          ; no-arg-parsing entry point
;       45:       3D0B001001       CMP EAX, 17825803
;       4A:       740F             JEQ L1
;       4C:       B80B001001       MOV EAX, 17825803
;       51:       89420F           MOV [EDX+15], EAX
;       54:       8BD0             MOV EDX, EAX
;       56: L0:   8BE5             MOV ESP, EBP
;       58:       F8               CLC
;       59:       5D               POP EBP
;       5A:       C3               RET
;       5B: L1:   8B420B           MOV EAX, [EDX+11]
;       5E:       83E804           SUB EAX, 4
;       61:       89420B           MOV [EDX+11], EAX
;       64:       8BD0             MOV EDX, EAX
;       66:       EBEE             JMP L0


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; sbcl-1.0.28でのdisassemble結果
; disassembly for IGO.CODE-STREAM:UNREAD
; 0A9F6AC5:       8D5C24F8         LEA EBX, [ESP-8]           ; no-arg-parsing entry point
;      AC9:       83EC0C           SUB ESP, 12
;      ACC:       8B55FC           MOV EDX, [EBP-4]
;      ACF:       8B05906A9F0A     MOV EAX, [#xA9F6A90]       ; #<FDEFINITION object for (SB-PCL::SLOT-ACCESSOR
                                                              ;                           :GLOBAL ..)>
;      AD5:       B904000000       MOV ECX, 4
;      ADA:       892B             MOV [EBX], EBP
;      ADC:       8BEB             MOV EBP, EBX
;      ADE:       FF5005           CALL DWORD PTR [EAX+5]
;      AE1:       81FA0B001001     CMP EDX, 17825803
;      AE7:       742F             JEQ L2
;      AE9:       8D5C24F8         LEA EBX, [ESP-8]
;      AED:       83EC0C           SUB ESP, 12
;      AF0:       BA0B001001       MOV EDX, 17825803
;      AF5:       8B7DFC           MOV EDI, [EBP-4]
;      AF8:       8B05946A9F0A     MOV EAX, [#xA9F6A94]       ; #<FDEFINITION object for (SB-PCL::SLOT-ACCESSOR
                                                              ;                           :GLOBAL ..)>
;      AFE:       B908000000       MOV ECX, 8
;      B03:       892B             MOV [EBX], EBP
;      B05:       8BEB             MOV EBP, EBX
;      B07:       FF5005           CALL DWORD PTR [EAX+5]
;      B0A:       7302             JNB L0
;      B0C:       8BE3             MOV ESP, EBX
;      B0E: L0:   BA0B001001       MOV EDX, 17825803
;      B13: L1:   8BE5             MOV ESP, EBP
;      B15:       F8               CLC
;      B16:       5D               POP EBP
;      B17:       C3               RET
;      B18: L2:   8D5C24F8         LEA EBX, [ESP-8]
;      B1C:       83EC0C           SUB ESP, 12
;      B1F:       8B55FC           MOV EDX, [EBP-4]
;      B22:       8B05986A9F0A     MOV EAX, [#xA9F6A98]       ; #<FDEFINITION object for (SB-PCL::SLOT-ACCESSOR
                                                              ;                           :GLOBAL ..)>
;      B28:       B904000000       MOV ECX, 4
;      B2D:       892B             MOV [EBX], EBP
;      B2F:       8BEB             MOV EBP, EBX
;      B31:       FF5005           CALL DWORD PTR [EAX+5]
;      B34:       83EA04           SUB EDX, 4
;      B37:       8BF2             MOV ESI, EDX
;      B39:       8975F8           MOV [EBP-8], ESI
;      B3C:       8BD6             MOV EDX, ESI
;      B3E:       8D5C24F8         LEA EBX, [ESP-8]
;      B42:       83EC0C           SUB ESP, 12
;      B45:       8B7DFC           MOV EDI, [EBP-4]
;      B48:       8B059C6A9F0A     MOV EAX, [#xA9F6A9C]       ; #<FDEFINITION object for (SB-PCL::SLOT-ACCESSOR
                                                              ;                           :GLOBAL ..)>
;      B4E:       B908000000       MOV ECX, 8
;      B53:       892B             MOV [EBX], EBP
;      B55:       8BEB             MOV EBP, EBX
;      B57:       FF5005           CALL DWORD PTR [EAX+5]
;      B5A:       7302             JNB L3
;      B5C:       8BE3             MOV ESP, EBX
;      B5E: L3:   8B75F8           MOV ESI, [EBP-8]
;      B61:       8BD6             MOV EDX, ESI
;      B63:       EBAE             JMP L1

明らかにdisassembleの結果が異なっている。
1.0.37ではcode-streamのフィールド(スロット)に直接アクセスしているのに対して、1.0.28ではsb-pcl::slot-accessor関数を通してのアクセスとなっている。
3倍近く差が出るのも納得。


unread関数(及び-ここでは出てきていないが-read関数)の定義を以下のように修正したところ、所要時間は36.374秒にまで縮まった。

;; symbol-macroletを使うように修正
(defun unread (code-stream)
  (declare (code-stream code-stream))
  (symbol-macrolet ((position   (position code-stream))
                    (surrogate? (surrogate? code-stream)))
    (if surrogate? 
        (setf surrogate? nil)
      (decf position))))

;; 1.0.28でのdisassemble結果
;; 1.0.37でwith-slotsを使った場合とほぼ同様
; disassembly for IGO.CODE-STREAM:UNREAD
; 0B89D892:       8B420F           MOV EAX, [EDX+15]          ; no-arg-parsing entry point
;       95:       3D0B001001       CMP EAX, 17825803
;       9A:       7510             JNE L1
;       9C:       8B420B           MOV EAX, [EDX+11]
;       9F:       83E804           SUB EAX, 4
;       A2:       89420B           MOV [EDX+11], EAX
;       A5:       8BD0             MOV EDX, EAX
;       A7: L0:   8BE5             MOV ESP, EBP
;       A9:       F8               CLC
;       AA:       5D               POP EBP
;       AB:       C3               RET
;       AC: L1:   B80B001001       MOV EAX, 17825803
;       B1:       89420F           MOV [EDX+15], EAX
;       B4:       8BD0             MOV EDX, EAX
;       B6:       EBEF             JMP L0

これでもまだ1.0.37との間には2秒ほどの差があるが、これはおそらくバージョンごとの基本性能の差。
1.0.34では1.0.28より1秒程度高速に、1.0.37では1.0.34よりさらに1秒程度高速になっていた。
バージョンが上がるごとに着実に速くなっていってるっぽいのが良い。