Commonlispのダイナミック変数

  • CLでためしにWiki書いてる。
  • 再帰的にかくとオーバーフローしそうなので反復的に書きたいが、反復的変換が難しい箇所が出てきた。
  • そこで、ダイナミック変数で何とかなるかなーと思ってテストしてみた。
;再帰版
(defun rec-tes1 (x)
  (if (= x 100000) ;1000とかだとうまく動く。
      0
      (+ x (rec-tes1 (+ x 1)))))

(rec-tes1 0)
;-> Control stack guard page temporarily disabled: proceed with caution 
; 0: (SB-KERNEL::CONTROL-STACK-EXHAUSTED-ERROR)
; 1: ("foreign function: call_into_lisp")
; 2: ("foreign function: post_signal_tramp")
; 3: (REC-TES1 65243)
; 4: (REC-TES1 65242)
; 5: (REC-TES1 65241)

;オーバーフローしたっぽい。


;ダイナミック変数束縛
(defparameter *result* 0)
(defun rec-tes2 (x)
  (if (= x 100000)
      *result*
      (let ((*result* (+ *result* x)))
        (rec-tes2 (+ x 1)))))

(rec-tes2 0)
;-> Control stack guard page temporarily disabled: proceed with caution 
; 0: (SB-KERNEL::CONTROL-STACK-EXHAUSTED-ERROR)
; 1: ("foreign function: call_into_lisp")
; 2: ("foreign function: post_signal_tramp")
; 3: (SB-KERNEL:TWO-ARG-+ 2128161420 65241)
; 4: (REC-TES2 65241)
; 5: (REC-TES2 65240)
; 6: (REC-TES2 65239)

;あら?オーバーフローした?
  • ご覧のとおり、ダイナミック変数に束縛し、末尾再帰呼び出し内でのみ環境を更新していく。
  • 関数の形自体は末尾再帰となっているので大丈夫かなーと思ったが・・・結果は再帰と同じ。
  • 落ちてる場所もほとんど同じみたい((REC-TES1 65243)と(REC-TES2 65241))だし、内部の構造はわからないが、内部では再帰的なアルゴリズムになっているのか、末尾再帰最適化がかからないのか。
  • とにかくパフォーマンスは再帰と同じようだから気をつけないといけない。
  • おちないように1000に変更して、時間も計ってみる。
(defun rec-tes1 (x)
  (if (= x 1000)
      0
      (+ x (rec-tes1 (+ x 1)))))

CL-USER> (time (dotimes (i 100000) (rec-tes1 0)))
;took : 3.420 seconds of real
;time : 3.420000 seconds of total run time (3.420000 user, 0.000000 system)                                                        

(defparameter *result* 0)
(defun rec-tes2 (x)  (if (= x 1000)
      *result*
      (let ((*result* (+ *result* x)))
        (rec-tes2 (+ x 1)))))

CL-USER> (time (dotimes (i 100000) (rec-tes2 0)))
;took : 2.930 seconds of real 
;time : 2.930000 seconds of total run time (2.930000 user, 0.000000 system)                                                        
  • なぜか速度はちょっと速いみたいw

なんとなく高速性を求めてみる。

末尾再帰型

(defun rec-tes3 (x result)
  (if (= x 1000)
      result
      (rec-tes3 (+ x 1) (+ result x))))

CL-USER> (time (dotimes (i 100000) (rec-tes3 0 0)))
;took : 2.100 seconds of real 
;time : 2.100000 seconds of total run time (2.100000 user, 0.000000 system)

組み込みのdotimes(もはや手続き)

(defun rec-tes4 (x)
  (progn (dotimes (i 1000) (setq x (+ x i))) x))

CL-USER> (time (dotimes (i 100000) (rec-tes4 0)))
;took : 0.870 seconds of real
;time : 0.870000 seconds of total run time (0.870000 user, 0.000000 system)

;disassembleしてみる
CL-USER> (disassemble 'rec-tes4)
; 0B84EE61:       31F6             XOR ESI, ESI               ; no-arg-parsing entry point
;       63:       EB18             JMP L2
;       65: L0:   8975F4           MOV [EBP-12], ESI
;       68:       8BD0             MOV EDX, EAX
;       6A:       8BFE             MOV EDI, ESI
;       6C:       E8D7127BF5       CALL #x1000148             ; GENERIC-+
;       71:       7302             JNB L1
;       73:       8BE3             MOV ESP, EBX
;       75: L1:   8BC2             MOV EAX, EDX
;       77:       8B75F4           MOV ESI, [EBP-12]
;       7A:       83C604           ADD ESI, 4
;       7D: L2:   81FEA00F0000     CMP ESI, 4000
;       83:       7CE0             JL L0
;       85:       8BD0             MOV EDX, EAX
;       87:       8D65F8           LEA ESP, [EBP-8]
;       8A:       F8               CLC
;       8B:       8B6DFC           MOV EBP, [EBP-4]
;       8E:       C20400           RET 4
;       91:       90               NOP
;       92:       90               NOP
;       93:       90               NOP
;       94:       90               NOP
;       95:       90               NOP
;       96:       90               NOP
;       97:       90               NOP
;       98:       CC0A             BREAK 10                   ; error trap
;       9A:       02               BYTE #X02
;       9B:       18               BYTE #X18                  ; INVALID-ARG-COUNT-ERROR
;       9C:       4D               BYTE #X4D                  ; ECX
;

さらに高速化

型宣言をつける。

(defun rec-tes5 (x)
  (declare (optimize (speed 3) (safety 0)))
  (declare (fixnum x))
  (progn (dotimes (i 1000) (setq x (the fixnum (+ x i))))
         x))

CL-USER> (time (dotimes (i 100000) (rec-tes5 0)))
;took : 0.150 seconds of real
;time : 0.150000 seconds of total run time (0.150000 user, 0.000000 system)

;かなり速くなった。
;初期と比べて20倍以上。

;disassembleしてみる
CL-USER> (disassemble 'rec-tes5)
; 0ABE96EC:       31C0             XOR EAX, EAX
;      6EE:       EB05             JMP L1
;      6F0: L0:   01C1             ADD ECX, EAX
;      6F2:       83C004           ADD EAX, 4
;      6F5: L1:   3DA00F0000       CMP EAX, 4000
;      6FA:       7CF4             JL L0
;      6FC:       8BD1             MOV EDX, ECX
;      6FE:       8D65F8           LEA ESP, [EBP-8]
;      701:       F8               CLC
;      702:       8B6DFC           MOV EBP, [EBP-4]
;      705:       C20400           RET 4


;rec-tes4と同じアルゴリズムだがだいぶ小さくなってる。
  • 最近はC言語もやっているので、最速と言われるC言語でつくってみる。
#include <stdio.h>
#include <time.h>

long int rec_tes6(long int t){
  int i = 0;
  while(i<1000){
    t += i;
    i++;
  }
  return t;
}

int main(){
  int i=0;
  clock_t t1;
  clock_t t2;

  t1 = clock();
  while(i<100000){
    long int x = rec_tes6(0);
    i++;
  }
  t2 = clock();

  printf("time = %10.30f\n",
         (double)(((double)t2 - t1)/CLOCKS_PER_SEC));

  return 0;
}
  • gccでコンパイル、実行。
$ gcc tes.c
$ ./a.out 
time = 0.560000000000000053290705182008
  • あれ?意外と遅い。
  • lispのほうが4倍程度速い。
  • アルゴリズムがまずいのかも。大体最速の書き方だと思うのだが・・・。

まとめ

  • そういえばダイナミック変数について。
  • ダイナミック変数束縛すると末尾再帰最適化がかからないみたいなので気をつける。