SBCL:究極のアセンブリコードブレッドボード(2014)

2026/05/21 0:39

SBCL:究極のアセンブリコードブレッドボード(2014)

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

主要な進展は、

NEXT
シーケンスの定義を洗練させ、無駄なアドレス計算を排除し、ハードコーディングされたアドレスではなく素朴なコードブロックに対してオフセットを符号化することで、マシンコードサイズを14バイトから9バイトに削減したことである。このアーキテクチャは、F18およびx87インスパイアードな設計など、プッシュ/ポップを実装するためにデータ移動ではなく制限されたモジュラーカウンターを用いるという小さなモジュールスタック(例えば F18 や x87 インスパイアード な設計)を活用しており、これにより素朴命令は暗黙的なスタック・カウンタ値に基づいて分岐先へ派遣し、分岐先の利点を活用した特殊化が可能となる。素朴命令はアリエイジング事故を最小限に抑えつつ、将来の進化のために柔軟性を維持するために固定間隔(64×67 = 4288 バイト)で格納されている。Steel Bank Common Lisp(SBCL)はこの特殊コードを発信する本質的なツールとして機能し、レジスタ(rsi/rax/rdi)に仮想指令ポインタを管理し、スタックスロット(r8–r15)を扱い、各素朴変種ごとに整列されたコードページを生成する。MacBook Air でのベンチマークでは、ネイティブアセンブリと初期のバイトコードを使用した場合、数百万ループ反復あたりのサイクル数は約150万から約60万に減少し、ベースレジスタへの依存なしで特殊な素朴命令を使用するスレッド型インタプリタにおいて大幅な速度向上を示している。

本文

【訂正】ルート・オイラー(Lutz Euler)氏が指摘した通り、次のシーケンス(NEXT)は、元々、ベースレジスタを伴わずにインデックスレジスタを使って実効アドレスをエンコードする用途にも使用されていました。このミスの影響で命令の意味が変わるわけではありませんが、無駄なエンコーディングを強いることになります。マシンのコードに見られる差異は以下の通りです。

以前(14 バイト): 1 2 3 4 ; 03: 8B043D00000000 MOV EAX, [RDI] ; 5 ユーティレスなバイト! ; 0A: 4883C704 ADD RDI, 4 ; 0E: 4801F0 ADD EAX, RSI ; 11: FFE0 JMP RAX

現在(9 バイト): 1 2 3 4 ; 93: 8B07 MOV EAX, [RDI] ; 95: 4883C704 ADD RDI, 4 ; 99: 4801F0 ADD EAX, RSI ; 9C: FFE0 JMP RAX

NEXT の定義は修正済みですが、以下のディスアSEMBリー(逆アセンブル)のスニペットは未だに旧来のマシンコードを示しています。

今週初め頃には、再び F18 というシステムにも目を通しました。チャック・ムーアの作品の場合いつものように、狂気に過ぎないかという程度の驚異的な才能かの区別が難しいというのが通例ですが;)一つ強く印象に残ったのはスタックの小ささでした:10 個のスロットしかなく、複雑なオーバーフローやアンダーフローのトラップがありません。その理由づけは、「もしもより多くのスロットが必要なら、やり方を間違えている」という点であり、沈黙するオーバーフローは、自分たちが何をしているか理解している際に有用だということです。これは確かに HP-41C や x87 での私の経験と一致します。また、djb 氏が私達による x87 の回転式スタックの誤用を嘆いた投稿にも思い起こさせられます。彼のテーゼは、「慎重なスケジューリングにより、『無料』な FXCH がスタックを同等であるか、それともそれに越したことはないほど優位なものにすることができる」というものでした。記事の最後には、x87 の暗黙的なスタック回転のおかげでデータをシャッフルするサイクルを浪费しない(非パイプライン化された)ループが記されています。

それは私に対し、スタックのスロット数を例えば 8 に制限するようなスタックベースの VM においてどのような実装技術が可能になるかを考えさせることになりました。理想的にはすべてレジスタ内に保ち続けることです。しかし、それを素朴に行うとプッシュとポップが大きく複雑化します;フォर्थエンジンが通常スタックの上端の要素のみを 1-2 つキャッシュしておく理由就在这里です。

そこで私は x87 と F18 を模倣することにしました(訂正:後者の TOS キャッシュレジスタ 2 つを除く);プッシュやポップはデータ移動を引き起こさないようにする代わりに、以下に示す図のように、スタックの先頭(TOS)を指すモジュラーカウンターを減算・加算します。これは依然としてソフトウェア上で遅いでしょう(大多数の ISA はレジスタへのインデックス化ができません)。重要な点は、このカウンターはあまり多くの値を取ることができないということです:スタックのスロットが 8 つしかない場合、つまり 8 の値しか持たないことです。スタック VM はいくつかのパフォーマンス向上のために既にプリミティブ関数を重複させています(例として、BTB の助けを借りて同じプリミティブの実行を複数のアドレスに分散させるなど),したがって、スタックカウンターが取ることをできたすべての 8 つの値に対して特別に設計されたプリミティブを作成することは合理的に見えるでしょう。

通常の直接スレッド化 VM では、大部分のプリミティブ関数は次の命令にジャンプするコードシーケンスで終わるものでした;何かのような add rsi, 8 ; ジャンプ前に仮想 IP をインCREMENT jmp [rsi-8] ; RSI が以前指していたアドレスにジャンプ ここで rsi は仮想命令ポインタであり、VM 命令は単に関連するプリミティブのマシンコードへのポインタです。

このシーケンスに対し私は 2 つの変更を行います。バイトコード内にハードコーディングされたアドレスを嫌うため、そして 1 命令あたり 64 ビットは過剰に浪费しています;代わりに、プリミティブ関数ブロックからのオフセットをエンコードすることにします: mov eax, [rsi] add rsi, 4 add rax, rdi jmp rax ここで rdi はプリミティブ関数の基本アドレスです。

また、暗黙的なスタックカウンターの新しい値に基づいてディスパッチする必要があります。各プリミティブ関数のバリアントを規則的な間隔(例:1 ページ)に格納することでディスパッチをできるだけ簡単にしたのです。私はそれを 64 * 67 = 4288 バイトで丸めました;エイリアシングの事故を最小限にするためです。NEXT は何かのようなものになります mov eax, [rsi] add rsi, 4 lea rax, [rax + rdi + variant_offset] jmp rax

トリックは、variant_offset = 4288 * stack_counter であり、スタックカウンターは(通常)プリミティブがコンパイルされる際に既知であることです。スタックをそのまま残せば、カウンターもそのままであり;値をプッシュするとカウンターを減算し、ポップすると増分します。

それはまあまあ妥当に見えるでしょう。それが機能するか見てみましょう。

予備的考察 (Preliminaries)

私がいかに多くの反復的なマシンコードを生成する必要があるかを探求したいと考えています。SLIME の REPL と SBCL のアセンブラはその任務に完璧です!(支援されていない内部を使っていることが明確であるかどうか、もし壊れても断片を保ってください。)

VM の基本設計は以下の通りです:

r8-r15: スロット(32 ビット) rsi: マシンコードプリミティブの基本アドレス rdi: 仮想命令ポインタ(次の命令を指す) rax, rbx, rcx, rdx: スクラッチレジスタ rsp: (仮想)リターンスタックポインタ。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

(import '(sb-assem:inst sb-vm::make-ea)) ; これら 2 つを多用します

;; 私たちのスタックの裏打ちストア (defvar stack (make-array 8 :initial-contents (list sb-vm::r8d-tn sb-vm::r9d-tn sb-vm::r10d-tn sb-vm::r11d-tn sb-vm::r12d-tn sb-vm::r13d-tn sb-vm::r14d-tn sb-vm::r15d-tn)))

;; プリミティブ関数生成時のスタックポインタ (defvar stack-pointer)

;; (@ 0) は現在の TOS レジスタを返す、(@ 1) はそれより下のものを返すなど (defun @ (i) (aref stack (mod (+ i stack-pointer) (length stack))))

(defvar code-base sb-vm::rsi-tn) (defvar virtual-ip sb-vm::rdi-tn)

(defvar rax sb-vm::rax-tn) (defvar rbx sb-vm::rax-tn) (defvar rcx sb-vm::rax-tn) (defvar rdx sb-vm::rax-tn)

;; バリアントは primitive-code-offset バイトだけ離れています (defvar primitive-code-offset (* 64 67))

;; 各 stack-pointer の値には独自のコードページがあります (defstruct code-page (alloc 0) ; 次の空きバイトのインデックス (code (make-array primitive-code-offset :element-type '(unsigned-byte 8))))

考えは、プリミティブ関数各々に対してアセンブリコードを生成する関数を定義することです;これらの関数は暗黙的に @ を通じて stack-pointer パラメータ化されます。私たちはそれらを必要に応じて呼び出すことができ、これにより stack-pointer のすべての値をカバーします。問題なのはコードシーケンスの長さが異なるため、すべてを同期に保つためにパディングを挿入する必要があることです;それが emit-code が行うことです:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

(defun emit-code (pages emitter) ;; コードページの数にはスタックスロットと同じ数必要です (assert (= (length stack) (length pages))) ;; 右端の開始点を見付け、16 バイトに整列します (let* ((alloc (logandc2 (+ 15 (reduce #'max pages :key #'code-page-alloc)) 15)) (bytes (loop for i below (length pages) for page = (elt pages i) collect (let ((segment (sb-assem:make-segment)) (stack-pointer i)) ;; この値のバリアントを生成するアセンブリコード ;; セグメントに挿入します (sb-assem:assemble (segment) ;; まずパディングを挿入 (sb-vm::emit-long-nop segment (- alloc (code-page-alloc page))) (funcall emitter)) ;; 戻る参照を整理 (sb-assem:finalize-segment segment) ;; 次に位置非依存のマシンコードとしてバイトベクタを取得 (sb-assem:segment-contents-as-vector segment))))) ;; 最後に、マシンの各コードシーケンスを適切なコードページにコピーします (map nil (lambda (page bytes) (let ((alloc (code-page-alloc page))) (replace (code-page-code page) bytes :start1 alloc) (assert (<= (+ alloc (length bytes)) (length (code-page-code page)))) (setf (code-page-alloc page) (+ alloc (length bytes))))) pages bytes) ;; そのコードシーケンスのオフセットを返します alloc))

この関数は emit-all-code によって一連のプリミティブ関数に対するマシンコードを生成する際に使用され、各プリミティブ関数の開始オフセットを追跡します。

1 2 3 4 5 6 7 8 9 10

(defun emit-all-code (&rest emitters) (let ((pages (loop repeat (length stack) for page = (make-code-page) ;; すべてのものをワンバイトの NOP で埋める do (fill (code-page-code page) #x90) collect page))) (values (mapcar (lambda (emitter) (emit-code pages emitter)) emitters) pages)))

さて、主役です: 1 2 3 4 5 6 7 8 9 10 11 12 13

(defun next (&optional offset) (setf offset (or offset 0)) ; IP を操作するプリミティブ関数を収容します (let ((rotation (mod stack-pointer (length stack)))) (inst movzx rax (make-ea :dword :base virtual-ip :disp offset)) (unless (= -4 offset) (inst add virtual-ip (+ 4 offset))) (if (zerop rotation) (inst add rax code-base) (inst lea rax (make-ea :qword :base code-base :index rax :disp (* rotation primitive-code-offset)))) (inst jmp rax)))

最初のステップ

いくつかの単純なプリミティブ関数を追加しましょう。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

(defun swap () (inst xchg (@ 0) (@ 1)) ; スタックの先頭とスタック [1] を交換します (next))

(defun dup () (decf stack-pointer) ; スタックを増やし(これは下方向に成長します) (inst mov (@ 0) (@ 1)) ; TOS を書き込みます (next))

(defun drop (&optional offset) (incf stack-pointer) ; ただスタックを縮めます (next offset))

(defun add () (inst add (@ 1) (@ 0)) ; 2 つ目の要素が TOS になります (drop))

(defun sub () (inst sub (@ 1) (@ 0)) (drop))

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 CL-USER> (setf print-length 100) 100 CL-USER> (emit-all-code 'swap 'dup 'drop 'add 'sub) (0 32 64 96 128) (#S(CODE-PAGE :ALLOC 152 :CODE #(69 135 193 139 4 61 0 0 0 0 72 131 199 4 72 1 240 255 224 102 15 31 132 0 0 0 0 0 15 31 64 0 69 139 248 139 4 61 0 0 0 0 72 131 199 4 72 141 132 6 64 117 0 0 255 224 15 31 132 0 0 0 0 0 139 4 61 0 0 0 0 72 131 199 4 72 141 132 6 192 16 0 0 255 224 102 15 31 132 0 0 0 0 0 102 144 69 1 193 139 ...)) ...) CL-USER> (defparameter code0 (code-page-code (first (second /)))) CODE0 CL-USER> (defparameter code1 (code-page-code (second (second //)))) CODE1

swap のコードは 0 バイトから 32 バイットの間にあります。スタックポインタ = 0 と 1 のバージョンを見てみましょう。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

CL-USER> (sb-sys:with-pinned-objects (code0) (sb-disassem:disassemble-memory (sb-sys:vector-sap code0) 32)) ; サイズ:32 バイト ; 0669C700: 4587C1 XCHG R8D, R9D ; 03: 8B043D00000000 MOV EAX, [RDI] ; 0A: 4883C704 ADD RDI, 4 ; 0E: 4801F0 ADD EAX, RSI ; 11: FFE0 JMP RAX ; 13: 660F1F840000000000 NOP ; パディング NOPs ; 1C: 0F1F4000 NOP NIL CL-USER> (sb-sys:with-pinned-objects (code1) (sb-disassem:disassemble-memory (sb-sys:vector-sap code1) 32)) ; サイズ:32 バイト ; 0669D810: 4587CA XCHG R9D, R10D ; 13: 8B043D00000000 MOV EAX, [RDI] ; 1A: 4883C704 ADD RDI, 4 ; 1E: 488D8406C0100000 LEA RAX, [RSI+RAX+4288] ; 26: FFE0 JMP RAX ; 28: 0F1F840000000000 NOP NIL

dup は 32-64 にあり、sub は 128-152 です: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

CL-USER> (sb-sys:with-pinned-objects (code0) (sb-disassem:disassemble-memory (sb-sys:sap+ (sb-sys:vector-sap code0) 32) 32)) ; サイズ:32 バイト ; 0669C720: 458BF8 MOV R15D, R8D ; 23: 8B043D00000000 MOV EAX, [RDI] ; 2A: 4883C704 ADD RDI, 4 ; 2E: 488D840640750000 LEA RAX, [RSI+RAX+30016] ; 36: FFE0 JMP RAX ; 38: 0F1F840000000000 NOP NIL CL-USER> (sb-sys:with-pinned-objects (code0) (sb-disassem:disassemble-memory (sb-sys:sap+ (sb-sys:vector-sap code0) 128) 24)) ; サイズ:24 バイト ; 0669C780: 4529C1 SUB R9D, R8D ; 83: 8B043D00000000 MOV EAX, [RDI] ; 8A: 4883C704 ADD RDI, 4 ; 8E: 488D8406C0100000 LEA RAX, [RSI+RAX+4288] ; 96: FFE0 JMP RAX NIL

これらは比較的緊密です。私はそこにどれほど少ないデータシャッフルがあるか確かに気に入っています;NEXT シーケンスはいくつかこじれがありますが、間接ジャンプは恐らくその最も弱い(かつ最も避けられない)点です。

制御フローのプリミティブ関数

制御フローがない VM は遊び物さえもではありません。まず、不条件相対的ジャンプです。これらは [jmp] [offset] とエンコードでき、32 ビットのオフセットはオフセットの終了から相対的です。私達はただ virtual-ip に新しいアドレスを上書きするだけです。

1 2 3 4 5

(defun jmp () (inst movsx rax (make-ea :dword :base virtual-ip)) (inst lea virtual-ip (make-ea :dword :base virtual-ip :index rax :disp 4)) (next))

コールとリターンはフォースエンジンにとって心臓部です。ret は簡単です:ただ制御スタックから virtual-ip にポップするだけです。 1 2 3

(defun ret () (inst pop virtual-ip) (next))

コールはいくぶん複雑です。jmp と似ていますが、次の命令のアドレスを制御スタックにプッシュします: 1 2 3 4 5 6

(defun call () (inst movsx rax (make-ea :dword :base virtual-ip)) (inst add virtual-ip 4) (inst push virtual-ip) (inst add virtual-ip rax) (next))

結果するマシンコードを見てみましょう。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

CL-USER> (emit-all-code 'jmp 'ret 'call) (0 32 64) (#S(CODE-PAGE :ALLOC 91 :CODE #(72 99 7 72 141 124 7 4 139 4 61 0 0 0 0 72 131 199 4 72 1 240 255 224 15 31 132 0 0 0 0 0 95 139 4 61 0 0 0 0 72 131 199 4 72 1 240 255 224 102 15 31 132 0 0 0 0 0 102 15 31 68 0 0 72 99 7 72 131 199 4 87 72 1 199 139 4 61 0 0 0 0 72 131 199 4 72 1 240 255 224 144 144 144 144 144 144 144 144 144 ...)) ...) CL-USER> (let ((code (code-page-code (first (second /))))) (sb-sys:with-pinned-objects (code) (sb-disassem:disassemble-memory (sb-sys:vector-sap code) 91))) ; サイズ:91 バイト ; 08395200: 486307 MOVSXD RAX, DWORD PTR [RDI] ; jmp ; 03: 488D7C0704 LEA RDI, [RDI+RAX+4] ; 08: 8B043D00000000 MOV EAX, [RDI] ; 0F: 4883C704 ADD RDI, 4 ; 13: 4801F0 ADD EAX, RSI ; 16: FFE0 JMP RAX ; 18: 0F1F840000000000 NOP ; 20: 5F POP RDI ; ret ; 21: 8B043D00000000 MOV EAX, [RDI] ; 28: 4883C704 ADD RDI, 4 ; 2C: 4801F0 ADD EAX, RSI ; 2F: FFE0 JMP RAX ; 31: 660F1F840000000000 NOP ; 3A: 660F1F440000 NOP ; 40: 486307 MOVSXD RAX, DWORD PTR [RDI] ; call ; 43: 4883C704 ADD RDI, 4 ; 47: 57 PUSH RDI ; 48: 4801C7 ADD RDI, RAX ; 4B: 8B043D00000000 MOV EAX, [RDI] ; 52: 4883C704 ADD RDI, 4 ; 56: 4801F0 ADD EAX, RSI ; 59: FFE0 JMP RAX

SBCL からの FFI

面白いデモのためにはほぼ十分なものが揃いました。唯一欠けているのは重要なのは、CL から VM へのコールです。呼出者が重要なレジスタを保存することを管理し、プリミティブ関数(rsi)と仮想 IP(rdi)レジスタが正しくセットアップされていることを前提にします。スタックは入力で充填され、rax が指すバッファから値をコピーされ、退出時に書き戻されます。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

(defun enter () (inst push sb-vm::rbp-tn) (inst mov sb-vm::rbp-tn sb-vm::rsp-tn) ; 通常のフレームをセットアップします (inst push rax) ; rax を一時保存します (dotimes (i 8) ; スタックをコピーして入れます (inst mov (@ i) (make-ea :dword :base rax :disp (* 4 i)))) (next))

(defun leave () ;; RAX を復元します (inst mov rax (make-ea :qword :base sb-vm::rbp-tn :disp -8)) ;; 出力スタックを上書きします (dotimes (i 8) (inst mov (make-ea :dword :base rax :disp (* 4 i)) (@ i))) ;; フレームを unwind します (inst mov sb-vm::rsp-tn sb-vm::rbp-tn) (inst pop sb-vm::rbp-tn) (inst ret))

enter の CL サイドの対話者は、VOP として続きます: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 (sb-c:defknown %enter-vm (system-area-pointer system-area-pointer system-area-pointer) (values) (sb-c:any) :overwrite-fndb-silently t)

(in-package "SB-VM") (sb-vm::define-vop (cl-user::%enter-vm) (:translate cl-user::%enter-vm) (:policy :fast-safe) (:args (stack :scs (sap-reg) :target rax) (code :scs (sap-reg) :target rdi) (primitives :scs (sap-reg) :target rsi)) (:arg-types system-area-pointer system-area-pointer system-area-pointer) (:results) (:temporary (:sc sap-reg :offset rax-offset :from (:argument 0)) rax) (:temporary (:sc sap-reg :offset rbx-offset :from :eval) rbx) (:temporary (:sc sap-reg :offset rcx-offset :from :eval) rcx) (:temporary (:sc sap-reg :offset rdx-offset :from :eval) rdx) (:temporary (:sc sap-reg :offset rdi-offset :from (:argument 1)) rdi) (:temporary (:sc sap-reg :offset rsi-offset :from (:argument 2)) rsi) (:ignore rbx rcx rdx) (:generator 0 (inst push r8-tn) ;; スタックは単に痛すぎて宣言できないほどでした (inst push r9-tn) ;; 仮定として... そしてそれは (inst push r10-tn) ;; スレッドベースの TN を書き込みます、これは regalloc が無視します。 (inst push r11-tn) (inst push r12-tn) (inst push r13-tn) (inst push r14-tn) (inst push r15-tn)

(move rax stack)
(move rdi code)
(move rsi primitives)
(inst call rsi)   ;; コードページが ENTER で始まると仮定します

(inst pop r15-tn)
(inst pop r14-tn)
(inst pop r13-tn)
(inst pop r12-tn)
(inst pop r11-tn)
(inst pop r10-tn)
(inst pop r9-tn)
(inst pop r8-tn)))

(in-package "CL-USER")

(defun %enter-vm (stack-sap bytecode-sap primitives-sap) (declare (type system-area-pointer stack-sap bytecode-sap primitives-sap)) (%enter-vm stack-sap bytecode-sap primitives-sap))

(defun vm (stack-designator bytecode primitives) (declare (type (simple-array (unsigned-byte 32) 1) bytecode) (type system-area-pointer primitives)) ;; スタックを設計要素で初期化します。 (let ((stack (make-array 8 :element-type '(unsigned-byte 32) :initial-element 0))) (if (typep stack-designator 'sequence) (map-into stack (lambda (x) (ldb (byte 32 0) x)) stack-designator) (fill stack (ldb (byte 32 0) stack-designator))) (sb-sys:with-pinned-objects (stack bytecode) (time (%enter-vm (sb-sys:vector-sap stack) (sb-sys:vector-sap bytecode) primitives))) stack))

残っているのは、プリミティブ関数のマシンコードを実行可能な範囲のメモリ内に保存することだけです。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

;; まず、実行可能な範囲のメモリを取得します (defvar code-page (sb-posix:mmap nil (* (length stack) primitive-code-offset) (logior sb-posix:prot-read sb-posix:prot-write sb-posix:prot-exec) (logior sb-posix:map-anon sb-posix:map-private) -1 0))

;; 私たちのプリミティブ関数名リスト (defvar primops '(enter leave swap dup drop add sub jmp call ret))

(defun assemble-primops () (multiple-value-bind (offsets pages) (apply 'emit-all-code primops) (loop for page in pages for offset upfrom 0 by primitive-code-offset do (sb-kernel:copy-ub8-to-system-area (code-page-code page) 0 code-page offset primitive-code-offset)) (mapcar 'cons primops offsets)))

;; この alist はプリミティブ関数名をオフセットにマッピングします (defparameter primops-offsets (assemble-primops))

1 2 3 CL-USER> primops-offsets ((ENTER . 0) (LEAVE . 64) (SWAP . 112) (DUP . 144) (DROP . 176) (ADD . 208) (SUB . 240) (JMP . 272) (CALL . 304) (RET . 336))

add sub を実行し(leave で終わります)見てみましょう。 1 2 3 4 5 6 7 8 9

CL-USER> (vm '(3 2 10) (coerce '(208 240 64) '(simple-array (unsigned-byte 32) 1)) code-page) Evaluation took: 0.000 seconds of real time 0.000003 seconds of total run time (0.000002 user, 0.000001 system) 100.00% CPU 2,288 processor cycles 0 bytes consed

#(5 0 0 0 0 0 3 5)

そして確かに、10 - (3 + 2) = 5 です。

我们也应该测试函数调用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

CL-USER> (vm '(3 2 10) (coerce '(304 8 ; call F 240 64 ;; F: 208 336) '(simple-array (unsigned-byte 32) 1)) code-page) Evaluation took: 0.000 seconds of real time 0.000005 seconds of total run time (0.000003 user, 0.000002 system) 100.00% CPU 2,640 processor cycles 0 bytes consed

#(5 0 0 0 0 0 3 5)

直接 add を実行するのではなく、このバイトコードシーケンスは call 命令から 8 バイト(2 dwords)後にあるものを呼び出します;私たちの場合、add ret です。

バイトコードを手書きするのは面倒です。この小さな関数は愚かしくもないことを管理します。 1 2 3 4 5 6 7

(defun assemble (opcodes) (map '(simple-array (unsigned-byte 32) 1) (lambda (opcode) (if (integerp opcode) (ldb (byte 32 0) opcode) (cdr (assoc opcode primops-offsets)))) opcodes))

私たちは今、次に書けます 1 2 3 4 5 6 7 CL-USER> (vm '(3 2 10) (assemble '(call 8 sub leave

                               add
                               ret))
         *code-page*)

条件分岐

今では基本的に直線コードや無限ループを書けます。私たちは条件分岐が必要です。その実装は jmp と似ていますが、小さなねじりがあります。ジャンプもし(スタックの先頭が)ゼロでなく、そしてゼロである場合からのジャンプから始めましょう。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

(defun jcc (cc) ;; 次のワードは条件を満たされる場合オフセットであり、そうでない場合は継続します。 (inst movsx rax (make-ea :dword :base virtual-ip)) (inst lea rax (make-ea :dword :base virtual-ip :index rax :disp 4)) (inst add virtual-ip 4) (inst test (@ 0) (@ 0)) ;; 更新します virtual-ip だけですゼロ/非ゼロの場合 (inst cmov cc virtual-ip rax) (next))

(defun jnz () (jcc :nz))

(defun jz () (jcc :z))

直近値なしではプログラムを書くのは難しいです。以前の制御フロープリミティブは仮想命令ストリーム内に即座なデータをエンコードしました。私たちは lit、inc、dec でも同じことを行います:

1 2 3 4 5 6 7 8 9 10 11 12

(defun lit () (decf stack-pointer) ; スタックを成長させます (inst mov (@ 0) (make-ea :dword :base virtual-ip)) ; 次のワードをロードします (next 4)) ; 次の命令に進み越します

(defun inc () (inst add (@ 0) (make-ea :dword :base virtual-ip)) (next 4))

(defun dec () (inst sub (@ 0) (make-ea :dword :base virtual-ip)) (next 4))

私の最初のループ

最後に、使い物にならない(もし役に立たなくても)良い見えるループを持っています。まず、プリミティブ関数コードページを更新します: 1 2 3 4 5 6 7 8

;; C-M-x を再評価を強制し、または defparameter (defvar primops '(enter leave lit swap dup drop add sub inc dec jmp jnz jz call ret))

(defvar primops-offsets (assemble-primops))

1 2 3 4 5 6 7 8 9 10 CL-USER> (vm '(1000000) (assemble '(lit 1 sub jnz -20 leave)) code-page) Evaluation took: 0.009 seconds of real time 0.009464 seconds of total run time (0.009384 user, 0.000080 system) 100.00% CPU 14,944,792 processor cycles 0 bytes consed

#(0 0 0 0 0 0 0 1)

この反則的なループの百万回反復、ただカウンターを減算するだけで 15M サイクルかかりました。1 サイクル/反復はそんなに悪くないです... 特にそれが 1 をロードした後、減算した後、そしてゼロと比較した後、間接ジャンプを実行しますことを考慮すると。

私たちは lit sub を dec に融合させることでより良くできます:

1 2 3 4 5 6 7 8 9 10 CL-USER> (vm '(1000000) (assemble '(dec 1 jnz -16 leave)) code-page) Evaluation took: 0.007 seconds of real time 0.006905 seconds of total run time (0.006848 user, 0.000057 system) 100.00% CPU 11,111,128 processor cycles 0 bytes consed

#(0 0 0 0 0 0 0 0)

すべてを融合させます!

カウンターを減算しゼロでなければジャンプすることは一般的な操作です(古い x86 はそれがハードウェアで実装した、loop で);decrement and jump if non-zero (djn) を VM に追加しましょう:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

(defun djn () (inst movsx rax (make-ea :dword :base virtual-ip)) (inst lea rax (make-ea :dword :base virtual-ip :index rax :disp 4)) (inst add virtual-ip 4) (inst sub (@ 0) 1) (inst cmov :nz virtual-ip rax) (next))

(defvar primops '(enter leave lit swap dup drop add sub inc dec jmp jnz jz djn call ret))

(defvar primops-offsets (assemble-primops))

1 2 3 4 5 6 7 8 9 10 CL-USER> (vm '(1000000) (assemble '(djn -8 leave)) code-page) Evaluation took: 0.005 seconds of real time 0.005575 seconds of total run time (0.005542 user, 0.000033 system) 120.00% CPU 8,823,896 processor cycles 0 bytes consed

#(0 0 0 0 0 0 0 0)

それはよくなります... しかし私は条件付き移動について本当に確信していません。分岐は通常予測可能なので、ハードウェアにそれを開示し、NEXT シーケンスを複製する意味があります。

1 2 3 4 5 6 7 8 9 10

(defun djn2 () (sb-assem:assemble () (inst sub (@ 0) 1) (inst jmp :z fallthrough) (inst movsx rax (make-ea :dword :base virtual-ip)) (inst lea virtual-ip (make-ea :dword :base virtual-ip :index rax :disp 8)) (next -4) ; virtual-ip を事前に増分しておきます fallthrough ; ASSEMBLE はラベルを解析します、TAGBODY のように (next 4)))

生成されたコードは大きくなりすぎず、2 つの間接ジャンプは 16 バイト離れています。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

; サイズ:64 バイト ; 00510220: 4183E801 SUB R8D, 1 ; 24: 7414 JEQ L0 ; 26: 486307 MOVSXD RAX, DWORD PTR [RDI] ; 29: 488D7C0708 LEA RDI, [RDI+RAX+8] ; 2E: 8B043DFCFFFFFF MOV EAX, [RDI-4] ; 35: 4801F0 ADD EAX, RSI ; 38: FFE0 JMP RAX ; 3A: L0: 8B043D04000000 MOV EAX, [RDI+4] ; 41: 4883C708 ADD RDI, 8 ; 45: 4801F0 ADD EAX, RSI ; 48: FFE0 JMP RAX ; 4A: 660F1F840000000000 NOP ; 53: 660F1F840000000000 NOP ; 5C: 0F1F4000 NOP

この代替実装は私たちの反則的なループでより良く機能します。 1 2 3 4 5 6 7 8 9 10

CL-USER> (vm '(1000000) (assemble '(djn2 -8 leave)) code-page) Evaluation took: 0.004 seconds of real time 0.004034 seconds of total run time (0.003913 user, 0.000121 system) 100.00% CPU 6,183,488 processor cycles 0 bytes consed

#(0 0 0 0 0 0 0 0)

それがどのように純粋なアセンブリコードと比較するか見てみましょう。 1 2 3 4 5 6

(defun ubench () (sb-assem:assemble () head (inst sub (@ 0) 1) (inst jmp :nz head) (next)))

1 2 3 4 5 6 7 8 9 10

CL-USER> (vm '(1000000) (assemble '(ubench leave)) code-page) Evaluation took: 0.000 seconds of real time 0.000629 seconds of total run time (0.000628 user, 0.000001 system) 100.00% CPU 1,001,904 processor cycles 0 bytes consed

#(0 0 0 0 0 0 0 0)

私の遅い MacBook Air は制御オーバーヘッドの 100%のループで 1 サイクル/反復得ます。djn2 で、合理的な特別化オペレータの良い実装では、ループはネイティブコードの約 6 倍遅いです。djn の worse な実装はまだネイティブコードの 8 倍しか遅くありませんが、ひどく特別化されていないバイトコードはネイティブコードの 11-15 倍遅いです。

判決 (Verdict)

スタックサイズを小さく制限する時に、仮想スタックポインタにプリミティブ関数を特別化するのは実践的に実現可能です。スレッド化インタプリタのためにそれなりのランタイムオーバーヘッドも持つように見えます。私は実際にはストレートスタック言語には興味を持っていません;しかしながら、ローカル変数との組み合わせで、固定スタック VM は美しいランタイム IR になると信じています。私たちは高水準言語をそのような VM 用のスーパーオペレータに翻訳する時間を発見できるかどうか見てみましょう。融合されたオペレーターは NEXT の重要性を減少させます;一方、単純な関数コール(アイテムを正しい位置で積み上げるためにシャッフルが少ないため)は依然として有用になります。

SBCL が分野特化のマシンコード生成を探求するための良い伴侶であることについて確かに証明されています。私はインタラクティブプログラミングとマシンコード生成(および検査)のためのそのようなサポートを持つ他の言語実装を知りません。FWIW、私は LuaJIT + dynasm はまもなく同等であると信じています。

Steel Bank Common Lisp:なぜなら C が抽象化しすぎることがあるため;)

同じ日のほかのニュース

一覧に戻る →

2026/05/20 22:43

GitHub、悪意のある Visual Studio Code エクステンションによる 3,800 リポジトリの侵害を確認。

## 日本語翻訳: GitHub は、従業員が悪意のある Visual Studio Code (VS Code) 拡張機能のインストールによって、約 3,800 の内部リポジトリが侵害された重大なセキュリティインシデントを確認しました。主な影響は、機密性の高い GitHub 内部のソースコードと、ハッカーグループ「TeamPCP」がサイバー犯罪フォーラムで所持していると主張する約 4,000 つのプライベートリポジトリの漏洩可能性がある点にあります。このインシデントは、ソフトウェアサプライチェーン内で長年見られる課題を浮き彫りにしており、過去には悪意のある拡張機能が認証情報を窃取したりマルウェアを配布したりするために利用されてきました。特に以前の年には、数百万回のインストール数を誇りながらセキュリティリスクにより削除されたトロージャン化された VS Code 拡張機能や、クリプトStealing の拡張機能、データを漏洩させる AI ベースのコーディングアシスタントを含む他のインシデントもありました。これら過去の事例は外部ユーザーを標的としたものであったのに対し、今回のインシデントは GitHub の自前のインフラストラクチャと開発チームに限定されており、これらの内部リポジトリの外に保存された顧客データに影響があったという証拠はありません。

2026/05/21 6:33

Google がウェブに対して「宣戦布告」に出たとして言われている。

## Japanese Translation: Google は、オープンウェブのリンクを AI が生成したサマリー(「AI オーバービュー」)に置換する活動を積極的に進めており、これは閉鎖的で管理されたインターネットを創出するというリスクを孕んでいます。現在、ウェブは開かれた文化的リソースとして扱われるべきですが、Google は自社の独占的なニーズを満たすために情報を文脈から切り離し、既存のデータを報酬なしの原材料として使用し、事実上その独り占め体制を支えています。このアプローチは、かつて企業が行ってきた「壁の中庭(walled gardens)」建設の試みに酷似しており、不制御された外部世界に対して安全でフィルタリングされたインターフェースを提供しています。この動きはリンクから LLM 生成のレスポンスへ移行するものであり、時々誤っている答えを提供することで、ウェブに対する Google の独占的な管理層を確立し、ウェブサイト、ユーザーの創作活動、デジタルアートが synthetic generation に餌を与える限り、それらを無関係にしてしまうという脅威をもたらしています。業界基準がこの優越性に適応して変化するリスクもあり、将来的には早期のアール「slopified AOL」に類似した、劣化しアルゴリズムによってキュレートされたプラットフォームとなる可能性があります。アクセスは Google のアルゴリズムが「関連する」と判断したコンテンツに限られます。主な影響は、デフォルト設定に依存するユーザーの情報多様性の低下です。真理および文化へのアクセスの独占に対抗するためには、個人はすぐにデジタル習慣を「De-googlify」する必要性に直面しており、代替検索エンジンへ移行し、Chrome を避ける必要があります。情報の独占化へのこのシフトはオープン標準にも脅威を与え、Google の「安全な」抽象化と対照的に、参加型ウェブに対して非難ラベルが適用される可能性があります。記事は Patreon での呼びかけで締めくくり、本文はクリエイティブ・コモンズアトリビューション・シェアアライク 4.0国際ライセンスの下で提供されていることを示しています。

2026/05/18 11:04

N トークン毎秒とは、実際にはどのような速度なのでしょうか?

## 概要: この報告書の中核的なメッセージは、ローカル LLM のパフォーマンスが単なる生速度だけでは決定されるのではなく、主にコンテンツタイプとハードウェアの制約に大きく依存することである。スループットベンチマークは、コード、テキスト、思考/推論、エージェントの 4 つの明確なストリーミングモードを明らかにしており、同等のトークン率においてでも識別子の高い密度のためプロース(文章)に比べてコードが著しく遅く感じられる。補足のために、本研究では長い識別子を分割する特定のパターン付き BPE(Piece-wise Byte Encoding)方式のトークナイゼーション手法を使用しており、例えば「processUserInput」は「process」「User」「Input」に分けられ、句読点や演算子もトークンとして扱われる。これは tiktoken や Claude のトークナイザーなど一般的なツールとは異なる。テスト範囲は広く、Raspberry Pi クラスのデバイスから 4090 などのハイエンド GPU、Groq などのクラウドサービスまでをカバーしている。重要な知見として、ハイエンドシステムではハードウェアが 1 秒あたり最大 800 トokens を処理できる一方で、実際のユーザー体験は人間の視覚処理速度で決定される上限に達し、極限スピードにおいてはボトルネックは計算能力からシフトして、人が画面を閲覧または視聴する速さという要因に移行する。したがって、これらのモデルを展開する企業は、これらの 4 つの特定のストリーミングモードに合わせて期待値と最適化戦略をカスタマイズし、ユーザーのニーズに合わせる必要がある。

SBCL:究極のアセンブリコードブレッドボード(2014) | そっか~ニュース