jit of guile

ちょっと前に、世界の終わりを示す世界終末時計が100秒(1分40秒)に設定されて話題になった。もう、分単位での表示なんてのんびり構えていられないので、秒表示にしたんだろう。

核戦争の脅威、気候変動による食料争奪戦勃発の危機なんかな。デブはデザート禁止ぐらいの処置を取れよ。最近読んだ本では、デザートは、カロリー爆弾って表現していましたから。

話は変わって、ねぇねぇ、この中で一番、時間厳守する大人はだーれ? そう、あの番宣番組ね。この間は、福山雅治君がご登場で番宣してたね。ゲストに番宣の枠が有るんだな。公共放送が聞いて呆れるぞ。

で、岡村君が名乗りを上げたので、ねぇねぇ、岡村。時計の文字盤はなんで、1から12までの数字なの? なんで1から10までにしなかったの?

12って数字は、キリストの弟子12人にちなんでいて、とっても聖なる数字だからー。

ボーと、生きてんじゃないよーー、と爆弾炸裂します。お約束ですね。で、国民は考える訳です。

答えは、計算が楽だったからーーーーーー。

12は、2でも3でも4でも6でも割り切れる。それに対して10は2と5でしか割り切れない。(こういう割り切れる数の事を約数って言うんだったとさりげなく、オイラーは知ってます)

その頃は、少数以下の数値を表す方法が発明されていなかった。だから、麦1俵、2俵と数えるしかない。現実的に不便。で、俵の半分とか、3等分したうちの1つとかって数えていた。こういう場合、色々な数で割り切れるのを利用した方が楽。

升12杯分で俵が丁度一つ出来上がるようにしておくと便利。余り大雑把も困るし、細かすぎても使い勝手が悪い。それで12が選ばれた。

12分割では大雑把過ぎて困る世界が有った。航海の時の天文測量と時刻。自船の位置を正確に算出出来ないと命に係わるからね。で、12の倍数で、切りの良い数を選んだ。そう、60ってやつ。丁度12の倍数になってて、計算とかも便利。更に、1年は365.xxx日だけど、60の倍数で360ってのも都合が良い。(1年を12に分割すると、大体1月が30日、余分な5.xxx日は、31日/月にして分散させたり、うるう年を設けたり)

かくして、12が基本になって、時刻や角度を表すってのが定着した。

GPSが当たり前の世の中になって、位置を、東経130度32分23秒とかで表すのは、コンピュータの世界では、逆に不便。よって、度以下は10進表記の小数点付きで表そうってのが主流。

でも、小数点で表すと数値を丸めている事になるんで、整数の比で表そうってのも健在。 Schemeなんかだと、頑なに、有理数って形式を要求してる。

んな事で、scheme話を続けます。

guile-2.2.6 self build on OpenBSD

OpenBSD(amd64)のpkgから入れたguileは、残念ながらdebug情報が剥ぎ取られている。で、自前で野良buildしてみた。

CC=cc ./configure
time gmake
 :
gmake[1]: Leaving directory '/tmp/guile-2.2.6'
   37m28.44s real    31m04.02s user     6m21.50s system

普通にconfigureしちゃうとgccが優先されて、おかしな所でエラーになるんで、ちゃんとclangを指定してあげた。gmakeはお約束として、ブーストで -j2 ぐらいを付けようとかと思ったけで、pkgのレシピを見たら、お勧めしないって書いてあったので、先人の知恵に倣った。過去にパラレルコンパイルで泣かされた経験があるよ。急げば回れとは良く言ったものだ。

(gdb) b main
Breakpoint 1 at 0x1340: file guile.c, line 91.
(gdb) r
Starting program: /usr/local/bin/guile

Breakpoint 1, main (argc=1, argv=0x7f7fffff40a8) at guile.c:91
91      {
(gdb) n
97        if (should_install_locale () && setlocale (LC_ALL, "") == NULL)
(gdb)
100       scm_install_gmp_memory_functions = 1;
(gdb)
101       scm_boot_guile (argc, argv, inner_main, 0);
(gdb)
GNU Guile 2.2.6
Copyright (C) 1995-2019 Free Software Foundation, Inc.
 :

ちゃんとgdb効果が出ていた。しめしめ。101行目のコードって、前回も出てきたね。 中に入って行くなら、ここから追えば いいね。(あっ、facebook用語を思わず使っちゃったけど、オイラーには全く関係がございません。)

#0  _thread_sys_read () at -:3
#1  0x0000008b077b85fe in _libc_read_cancel (fd=0, buf=0x88b14cfc50, nbytes=1)
    at /usr/src/lib/libc/sys/w_read.c:27
#2  0x0000008b66fc0a3d in fport_read (port=<optimized out>,
    dst=<optimized out>, start=<optimized out>, count=1) at fports.c:573
#3  0x0000008b67003ee9 in scm_i_read_bytes (port=0x88b14d1ba0,
    dst=0x88b14cfc30, start=0, count=1) at ports.c:1559
#21 0x0000008ad2946f59 in GC_call_with_stack_base ()
    from /usr/local/lib/libgc.so.4.0
     :
#22 0x0000008b670410a8 in scm_i_with_guile (func=<optimized out>,
    data=<optimized out>, dynamic_state=<optimized out>) at threads.c:704
#23 scm_with_guile (func=<optimized out>, data=<optimized out>)
    at threads.c:710
#24 0x0000008b66fceb18 in scm_boot_guile (argc=1, argv=0x7f7fffff40a8,
    main_func=0x88b13f4440 <inner_main>, closure=0x0) at init.c:324
#25 0x00000088b13f4414 in main (argc=1, argv=0x7f7fffff40a8) at guile.c:101

S式の入力待ちの時の状況。結構、深いな。

benchmark

guile-3.0の中を伺っていると、NEWSなんてニュースレターが置いてあった。折角なんで購読してみると、

** Just-in-time code generation

Guile programs now run up to 4 times faster, relative to Guile 2.2,
thanks to just-in-time (JIT) native code generation.  Notably, this
brings the performance of "eval" as written in Scheme back to the level
of "eval" written in C, as in the days of Guile 1.8.

See "Just-In-Time Native Code" in the manual, for more information.  JIT
compilation will be enabled automatically and transparently.  To disable
JIT compilation, configure Guile with `--enable-jit=no' or
`--disable-jit'.  The default is `--enable-jit=auto', which enables the
JIT if it is available.  See `./configure --help' for more.

JIT compilation is enabled by default on x86-64, i686, ARMv7, and
AArch64 targets.

既存(guile 2.2)に比べて、4倍速くなったとな。ネイティブコードに自動変換するようにしたから。但し使える石は限定されるけどって案内。そう言われると真偽を確かめたくなる。

一番差が出そうな奴を登場させる。オリンピックには多種多様な競技があるけど、皆身体能力の異なる部分を見てるから、全能な試験じゃ無い事をあらかじめ心得ておく。結果が独り歩きしないようにね。

そう言えば、健康診断の前日は、お酒を控えましょう、油っぽい食事は避けて、早めに食べましょうってのが有ったな。これって、厚生労働省からの差し金ですかね? 良い記録が出て、医療費を圧縮したい魂胆が見え隠れしています。まあ、世の中はそんなものよ。

(define (fibo n)
  (if (or (= n 0) (= n 1))
      1
      (+ (fibo (- n 1))
         (fibo (- n 2)))))

で、実行時間を計測するtimeマクロがguileには無い。どうする? そんなの簡単。gaucheからコードを頂いてきて、guileで動くようにしちゃえ。(from Gauche/lib/gauche/time.scm)

(define-syntax %with-times
  (syntax-rules ()
    ((_ expr exprs do-result)
     (begin
       (gc)
       (let* (
              (st (gettimeofday))
              (r (begin expr . exprs))
              (sp (gettimeofday))
              (sreal-sec (car st)) (sreal-msec (cdr st))
              (ereal-sec (car sp)) (ereal-msec (cdr sp)))
         (let ((real (- (+ ereal-sec (/ ereal-msec 1000000.0))
                        (+ sreal-sec (/ sreal-msec 1000000.0)))))
           (do-result r real)))))))

(define-syntax rt
  (syntax-rules ()
    ((_ expr . exprs)
     (%with-times expr exprs
                  (lambda (r real )
                     (format (current-error-port)
                             ";~s\n; real ~a\n"
                             '(time expr . exprs) real)
                     r)))
    ((_)
     (syntax-error "usage: (time expr expr2 ...); or you meant sys-time?"))))

オリジナルは、もっと細かいデータが取れたり、多値を返すS式にも対応してたりするんだけど、取り合えずの間に合わせです。

GNU Guile 3.0.0  ;; at debian(32Bit)
scheme@(guile-user)> (rt (fibo 40))
;(time (fibo 40))
; real 5.715630054473877
$6 = 165580141
GNU Guile 2.2.4     30.755325078964233
Chez Scheme 9.5.3    3.456421559
Petite Chez 9.5.3   35.035922578
gosh 0.9.9          24.276

確かにguile同士で比べると5倍は速くなってる。この恩恵を受けたいならバージョンアップする事に限るな。で、世界には更に速いやつが居る。Chezファミリーだ。Chezはコンパイラー、Petiteはインタープリター版。10倍の差が付いている。

goshはたゆまぬ自助努力で、素晴らしい健闘をしてますねぇ。普通に使うなら、機能が充実してて資料豊富なgoshがお勧めだな。

cache

goshとかで計測する時、fibo.scmにtimeを追加してた。それを元に戻した。そして、ロードしたら、こんな案内が出てきた。

scheme@(guile-user)> (load "./fibo.scm")
;;; note: source file /tmp/t/./fibo.scm
;;;       newer than compiled /home/sakae/.cache/guile/ccache/3.0-LE-4-4.2/tmp/t/fibo.scm.go
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;;       or pass the --no-auto-compile argument to disable.
;;; compiling /tmp/t/./fibo.scm
;;; compiled /home/sakae/.cache/guile/ccache/3.0-LE-4-4.2/tmp/t/fibo.scm.go

読み込んだ時にコンパイルされてキャッシュされるとな。

debian:ccache$ tree
.
├── 2.2-LE-4-3.A
│   ├── home
│   │   └── sakae
│   └── tmp
│       └── t
│           ├── fibo.scm.go
│           └── rt.scm.go
└── 3.0-LE-4-4.2
    ├── home
    │   └── sakae
    └── tmp
        ├── hoge
        │   └── shooting-game
        │       ├── game.scm.go
        │       ├── que-std.scm.go
        │       ├── stp-wt-guile.scm.go
        │       └── vt100-kbd.scm.go
        └── t
            ├── fibo.scm.go
            └── rt.scm.go

何処で何をやってたか、記録が取られている。これはやばいよ。

debian:ccache$ cd 3.0-LE-4-4.2/tmp/t
debian:t$ ls -l
total 140
-rw-r--r-- 1 sakae sakae 67245 Jan 28 07:20 fibo.scm.go
-rw-r--r-- 1 sakae sakae 71693 Jan 28 06:42 rt.scm.go
debian:t$ file fibo.scm.go
fibo.scm.go: ELF 32-bit LSB shared object, no machine, version 1 (embedded), dynamically linked, with debug_info, not stripped

図体が大きい。しかもオブジェクトファイル。マシンと言うか石からも独立してる。きっと、guileのVMだろうね。

scheme@(guile-user)> ,x fibo
Disassembly of #<procedure fibo (n)> at #xb4c9a0e0:                             
   0    (instrument-entry 16358)        at /tmp/t/./fibo.scm:1:0
   2    (assert-nargs-ee/locals 2 6)    ;; 8 slots (1 arg)
   3    (make-short-immediate 7 2)      ;; 0
   4    (=? 6 7)                        at /tmp/t/./fibo.scm:2:10
   5    (je 49)                         ;; -> L3
   6    (make-short-immediate 7 6)      ;; 1                                       7    (=? 6 7)                        at /tmp/t/./fibo.scm:2:18
   :
  55    (reset-frame 1)                 ;; 1 slot
  56    (handle-interrupts)
  57    (return-values)

これがVM語か。

time

上で急造したtimeもどきのrtなんだけど、もう用が済んだんで、のんびりとソースを散策してますよ。そしたら、module/ice-9/time.scmなんてのを発見したぞ。

;; This module exports a single macro: `time'.
;; Usage: (time exp)
;;
;; Example:
;; guile> (time (sleep 3))
;; clock utime stime cutime cstime gctime
;; 3.01  0.00  0.00   0.00   0.00   0.00
;; 0

遅きに失したな。気を取り直して、

scheme@(guile-user)> (time (sleep 3))
;;; <stdin>:1:0: warning: possibly unbound variable `time'
ice-9/boot-9.scm:1669:16: In procedure raise-exception:
Unbound variable: time

gaucheから移植を決意させたのは、このtime無しが発端です。

何時でも使える訳では無いのね。use-moduleで、使いたい使いたいとおねだりしろとな。マニュアルには、きちんと書いてないし、ソースを読んだ人の特典って感じで、不親切だな。

scheme@(guile-user)> (use-modules (ice-9 time))
scheme@(guile-user)> (time (sleep 3))
clock utime stime cutime cstime gctime
 3.00  0.00  0.00   0.00   0.00   0.00
$1 = 0

今はCentOS上のguile-3.0なんで、記念に確認してみる。

scheme@(guile-user)> (time (fibo 40))
clock utime stime cutime cstime gctime
 4.50  4.46  0.00   0.00   0.00   0.00
$3 = 165580141

時間のねたは(times)ってのが、一括管理してるようだ。

      -- Scheme Procedure: tms:clock tms
          The current real time, expressed as time units relative to an
          arbitrary base.
      -- Scheme Procedure: tms:utime tms
          The CPU time units used by the calling process.
      -- Scheme Procedure: tms:stime tms
          The CPU time units used by the system on behalf of the calling
          process.
      -- Scheme Procedure: tms:cutime tms
          The CPU time units used by terminated child processes of the
          calling process, whose status has been collected (e.g., using
          ‘waitpid’).
      -- Scheme Procedure: tms:cstime tms
          Similarly, the CPU times units used by the system on behalf of
          terminated child processes.

cutimeとか、最初意味が分からなかったけど、これで納得したよ。

scheme@(guile-user)> (times)
$4 = #(4296985890000000 8950000000 40000000 0 0)
 :
scheme@(guile-user)> (times)
$6 = #(4297565440000000 13350000000 50000000 0 0)

課金に最適なカウンターです。

jitの発動

GUILE_JIT_THRESSOLDとか GUILE_JIT_LOGって環境変数で、発動タイミングを決めたり、ログのレベルを変えられるとな。libguile/jit.c

scm_init_jit (void)
{
  scm_jit_counter_threshold = scm_getenv_int ("GUILE_JIT_THRESHOLD",
                                              default_jit_threshold);
  jit_stop_after = scm_getenv_int ("GUILE_JIT_STOP_AFTER", -1);
  jit_pause_when_stopping = scm_getenv_int ("GUILE_JIT_PAUSE_WHEN_STOPPING", 0);
  jit_log_level = scm_getenv_int ("GUILE_JIT_LOG", 0);
}

初期値は1000回に設定されてた。しつこく回るとコンパイルしてくれるのね。

(base) [sakae@c8 tmp]$ export GUILE_JIT_LOG=2
(base) [sakae@c8 tmp]$ guile
jit: allocated code arena, 0x7f3cd2bdf000-0x7f3cd2c1f000
jit: mcode: 0x7f3cd2bdf000,+54
jit: mcode: 0x7f3cd2bdf040,+53
jit: mcode: 0x7f3cd2bdf080,+11
jit: vcode: start=0x137b2b8,+6 entry=+0
jit: Instruction first seen at vcode 0x137b2b8: instrument-entry
jit: Instruction first seen at vcode 0x137b2c0: assert-nargs-ee
jit: Instruction first seen at vcode 0x137b2c4: subr-call
jit: Instruction first seen at vcode 0x137b2c8: handle-interrupts
jit: Instruction first seen at vcode 0x137b2cc: return-values
jit: mcode: 0x7f3cd2bdf090,+220
jit: vcode: start=0x7f3cd59ec8bc,+48 entry=+0
jit: Instruction first seen at vcode 0x7f3cd59ec8c4: assert-nargs-ee/locals
jit: Instruction first seen at vcode 0x7f3cd59ec8c8: immediate-tag=?
 :
scheme@(guile-user)> (load "fibo.scm")
jit: vcode: start=0x1378b20,+6 entry=+0
jit: mcode: 0x7f3cd1c722c0,+216
jit: vcode: start=0x7f3cd293bf48,+50 entry=+0
jit: mcode: 0x7f3cd1c723a0,+745

それぞれの関数について1000回実行されると、自動的にmcodeとやらに変換されるんだな。

変換の現場を捉えてみるかな。

Thread 1 "guile" hit Breakpoint 1, 0x00007f28702fb81a in compute_mcode (
    data=<optimized out>, entry_ip=<optimized out>, thread=<optimized out>)
    at jit.c:5678
5678          if (vcode_start == thread->vm.ip)
(gdb) bt
#0  0x00007f28702fb81a in compute_mcode (data=<optimized out>,
    entry_ip=<optimized out>, thread=<optimized out>) at jit.c:5678
#1  scm_jit_compute_mcode (thread=0x22acd80, data=0x2150b38) at jit.c:5695
#2  0x00007f2870354b96 in vm_debug_engine (thread=0x22acd80) at vm-engine.c:370
#3  0x00007f28703577fd in scm_call_n (proc=<optimized out>,
    argv=argv@entry=0x7ffd7c807628, nargs=nargs@entry=1) at vm.c:1589
#4  0x00007f28702d4b97 in scm_primitive_eval (exp=<optimized out>,
    exp@entry=0x232b250) at eval.c:671
#5  0x00007f28702d4bf3 in scm_eval (exp=0x232b250,
    module_or_state=module_or_state@entry=0x232df00) at eval.c:705
#6  0x00007f287032db30 in scm_shell (argc=1, argv=0x7ffd7c807c88)
    at script.c:357
 :

引数がそれぞれoptimizedになってるのは残念だけど、仕組みが分かってきた(積り)になっておこう。

Guile Implementation

上の方で、hoge.scm.goなんてのが出てきた。知らないと、何でgolangと思っちゃうんだけど、 素性調査でELFって事が分かったんだった。すると、今度は何でELFなのって疑問が出て来る。

その答えが、 Object File Formatに解説されてた。要はELFフォーマットにほれ込みました。だから、車輪の再発明はしませんって事だ。それだけ汎用性に富んだフォーマットとも言えるな。

そしてバーチャルマシンの Instruction Set が、解説されてた。

Just-In-Time Native Codeによると、テンプレートJITって事らしい。素直なやつ。何処かの石メーカーみたいに、投機実行なんてのには、今の所手を出していない。それでも、十分に速いものに仕上がっているんで、後は対象の石を増やした方が良いのではないでしょうか。そう、喜びは皆で分かち合いましょう。えこひいきは、争いの元です。

所で、ruby方面もJITの嵐が吹き荒れているみたいだけど、報告を乞う。調子が良いなら、遅いクリスマスプレゼントを受けてもいいぞ。

readline

素のreplは貧弱。readlineを下記のように有効にすると、

scheme@(guile-user)> (use-modules (ice-9 readline))
scheme@(guile-user)> (activate-readline)
scheme@(guile-user)> (define (aa n)
... (+ n n n))
scheme@(guile-user)> (proceTAB TAB
procedure                procedure-minimum-arity  procedure-source
procedure?               procedure-name           procedure-with-setter?
procedure-arguments      procedure-properties     process-use-modules
procedure-documentation  procedure-property

これが好みなら、.guileに設定しておけとな。ヒストリーも取られるぞ。悪行も記録されるんで、困る事もあろうか。

scheme@(guile-user)> (readline-options 'help)
history-file            yes     Use history file.
history-length          200     History length.
bounce-parens           500     Time (ms) to show matching opening parenthesis (0 = off).
bracketed-paste         yes     Disable interpretation of control characters in pastes.

jit off

説明書によると、環境変数の設定により、jitを無効に出来るそうな。これで、同じ土俵で、有効度を比較出来る。

(base) [sakae@c8 tmp]$ export GUILE_JIT_THRESHOLD=-1
  :
scheme@(guile-user)> (time (fibo 40))
clock utime stime cutime cstime gctime
14.75 14.64  0.00   0.00   0.00   0.00

下記は、有効時の結果

scheme@(guile-user)> (time (fibo 40))
clock utime stime cutime cstime gctime
 4.39  4.36  0.00   0.00   0.00   0.00

3.3倍の違いとなった。環境によって差が出るんで、あくまで目安だな。普通にデフォで使えばよい。動作が怪しいと思ったらoffだな。更に、jitが何処でbugってるか調べる、環境変数も用意されてた。そろりそろりと使えって事かな。

~/.guile

(base) [sakae@c8 ~]$ cat .guile
(use-modules (ice-9 time))
(use-modules (ice-9 readline))
(activate-readline)

本日のドットファイル。これから、モジュールを精査して、便利そうなのを取り込んでいく積り。