Posted 2015-06-18 19:00:49 GMT

配列やハッシュテーブルを関数として利用できる機構は1970年代位からあるようですが、最近だとClojureやArcなどが活用しています。

Common Lispでもそんな感じのライブラリはあったりはするのですが、簡単に定義もできるので適当に試して遊んでみていました。 例えば、レキシカル変数を共有すれば、こんな感じで定義できます。

defmacro defdict/closure ( name &rest keys&vals ) ` let list ,@keys&vals plist-hash-table tab key nil keyp &optional gethash key tab if keyptab defun ,name defun setf ,name ( val key ) setf gethash key tab val ',name (defdict/closure d0 :a 0 :b 1 :c 2) (d0 :a) (d0 :d) (setf (d0 :a) 42) (d0 :a)

この辺りはLisp-1だと綺麗な気もしますが、まあそれは置いておきます。

折角なのでインライン展開して欲しい

この方式はまずまず良いのですが、トップレベルの defun でないとインライン展開してくれないことが殆どです。

上記の場合は、 let の中に defun があるので大抵はコンパイラはインライン展開を諦めます。

これを避けるには、大域で定義する他ありません。

ということで、こんな感じになりました。

インライン展開もされるし、まずまず良いのですが、 disassemble してみると、 gethash と比べて、どうも長いのが気になります。

d1

; disassembly for d1 (assembled 130 bytes) L0: mov rcx, [r12+96] ; thread.binding-stack-pointer mov [rbp-16], rcx cmp r8, 537919511 jne L1 mov rax, [rip-139] ; '#:|dict-1521| mov edx, [rax-11] mov rdx, [r12+rdx] cmp edx, 97 cmoveq rdx, [rax-7] cmp edx, 81 jeq L2 mov rsp, rbp clc pop rbp ret L1: mov rax, [rip-172] ; '#:|dict-1521| mov edi, [rax-11] mov rdi, [r12+rdi] cmp edi, 97 cmoveq rdi, [rax-7] cmp edi, 81 jeq L3 mov rdx, r9 mov esi, 537919511 mov rax, [rip-199] ; #<FDEFINITION for sb-impl::gethash3> mov ecx, 6 push qword ptr [rbp+8] jmp qword ptr [rax+9] mov r9d, 537919511 ; :optional entry point mov r8d, 537919511 jmp L0 break 16 ; Invalid argument count trap L2: break 10 ; error trap byte #X02 byte #X1B ; UNBOUND-SYMBOL-ERROR byte #X1B ; RAX L3: break 10 ; error trap byte #X02 byte #X1B ; UNBOUND-SYMBOL-ERROR byte #X1B ; RAX

gethash はこんな程度です。

gethash

; disassembly for gethash (assembled 37 bytes) L0: mov rdx, r10 ; no-arg-parsing entry point mov rdi, r9 mov rsi, r8 mov rax, [rip-246] ; #<FDEFINITION for sb-impl::gethash3> mov ecx, 6 push qword ptr [rbp+8] jmp qword ptr [rax+9] mov r8d, 537919511 ; :optional entry point jmp L0 break 16 ; Invalid argument count trap

defglobalを使ってみる

何が長くなる原因なのかなと考えましたが、ハッシュテーブルをスペシャル変数に格納しているのが原因かなと考えました。

大域変数に格納するとなると、スペシャル変数か定数かしか選択肢はないのですが、スペシャル変数の場合、ローカルに束縛される可能性があるので、それ用のコードが含まれることになります。

今回の場合、格納用の変数が束縛される必要はないので無駄だなあ、と思っていたのですが、もしや、こういう場合に、 defglobal を使うのではないだろうかと思い、 defglobal に置き換えてみました。

defglobal とは、スペシャル変数なんだけれども、再束縛はできず、値の変更は代入でしかできない変数を宣言するものです。

ANSI Common Lispには含まれていませんが、Lispマシンの時代からある機能で、定番拡張となっていて、大抵の処理系には含まれています。

; disassembly for d2 (assembled 90 bytes) L0: mov rcx, [r12+96] ; thread.binding-stack-pointer mov [rbp-16], rcx cmp r8, 537919511 jne L1 mov rax, [rip-151] ; '#:|dict-0| mov rdx, [rax-7] mov rsp, rbp clc pop rbp ret L1: mov rax, [rip-168] ; '#:|dict-0| mov rdi, [rax-7] mov rdx, r9 mov rsi, [rip-174] ; :default mov rax, [rip-173] ; #<FDEFINITION for sb-impl::gethash3> mov ecx, 6 push qword ptr [rbp+8] jmp qword ptr [rax+9] mov r9d, 537919511 ; :optional entry point mov r8d, 537919511 jmp L0 break 16 ; Invalid argument count trap

予想どおり大分すっきりしました。

インライン展開もされるので、 gethash を手書きした場合とほぼ同じです。

( ) declare optimize speed 3 safety 0 compilation-speed 0 dotimes ( i 100 ( d3 :q ) ) incf the fixnum ( d3 :q ) defun foo xor r8, r8 jmp L1 nop L0: mov [rbp-8], r8 mov rax, [rip-139] mov rdi, [rax-7] lea rbx, [rsp-16] sub rsp, 24 mov rdx, [rip-151] mov rsi, [rip-150] mov rax, [rip-149] mov ecx, 6 mov [rbx], rbp mov rbp, rbx call qword ptr [rax+9] mov r8, [rbp-8] mov rsi, rdx add rsi, 2 mov [rbp-8], r8 mov rax, [rip-209] mov rdi, [rax-7] lea rbx, [rsp-16] sub rsp, 24 mov rdx, [rip-221] mov rax, [rip-204] mov ecx, 6 mov [rbx], rbp mov rbp, rbx call qword ptr [rax+9] mov r8, [rbp-8] lea rcx, [r8+2] mov r8, rcx L1: cmp r8, 200 jl L0 mov rax, [rip-281] mov rdi, [rax-7] mov rdx, [rip-284] mov rsi, [rip-283] mov rax, [rip-282] mov ecx, 6 push qword ptr [rbp+8] jmp qword ptr [rax+9]

手書きした場合

defglobal *h* make-hash-table (defun bar () (declare (optimize (speed 3) (safety 0) (compilation-speed 0))) (dotimes (i 100 (gethash :q *h* 0)) (incf (the fixnum (gethash :q *h* 0))))) xor r9, r9 jmp L1 nop L0: mov [rbp-8], r9 mov rax, [rip-123] mov r8, [rax-7] mov [rbp-16], r8 mov rdi, r8 lea rbx, [rsp-16] sub rsp, 24 mov rdx, [rip-142] xor esi, esi mov rax, [rip-143] mov ecx, 6 mov [rbx], rbp mov rbp, rbx call qword ptr [rax+9] mov r8, [rbp-16] mov rsi, rdx add rsi, 2 lea rbx, [rsp-16] sub rsp, 24 mov rdi, r8 mov rdx, [rip-195] mov rax, [rip-186] mov ecx, 6 mov [rbx], rbp mov rbp, rbx call qword ptr [rax+9] mov r9, [rbp-8] lea rcx, [r9+2] mov r9, rcx L1: cmp r9, 200 jl L0 mov rax, [rip-255] mov rdi, [rax-7] mov rdx, [rip-258] xor esi, esi mov rax, [rip-259] mov ecx, 6 push qword ptr [rbp+8] jmp qword ptr [rax+9]

まとめ

以前から defglobal の使い道ってあるのかなあと思ったりしていましたが、再束縛する必要はない大域変数というのもそれなりにある気がするので、実は使い所は多かったりするのかもしれません。

■

