在本文开始之前,先祝贺 Emacs China 的各位道友 2023 新年快乐!
前言
我们知道,在很多系统级编程语言,如 C/C++ 、 Rust 里,编译器往往会提供一些语法与机制用于内联汇编,能够允许程序员直接操作寄存器或执行机器码,达到更高的执行效率或进行更细粒度的底层控制。想象一下,如果动态语言能够做到这一点会是什么样呢? SBCL 作为 Common Lisp 直接到机器码的编译器实现,可以说十分擅长在 Lisp 程序里插入汇编程序段,这样除了可以获得上述的好处之外,个人认为最大好处莫过于能在进行底层编程的同时,享受交互式的编程编程体验,也就是说修改代码后不再需要等待漫长的编译过程(这里点名批评 Rust)与重复地进行数据输入,即可得到代码的运行结果并检验其正确性。
国外也有一些文章介绍了在 SBCL 里进行内联汇编的方法,但我认为其中的讲解过于复杂且脱离实际用途,并且用到的 SBCL 内部 API 很多已经被弃用了,我这里以实际的开发案例,演示一下在 SBCL 里面向机器编程的方法。
SBCL 的优化机制
这里以 Common Lisp 中 float-sign
这个函数为例,为什么以它为例呢?因为本人之前文章 记录一次将 C++ 程序移植到 Common Lisp 上的经历 - #12,来自 coco24 提到的工程里,通过内联汇编优化 float-sign
这个热点函数,实现了显著的性能提升。
这个函数做的事情很简单,如果传入一个浮点数,返回这个浮点数的符号,如果传入两个参数,则返回第一个参数的符号赋给第二个参数后的结果:
(float-sign -3.14) ; => -1.0
(float-sign -3.14 1.592) ; => -1.592
(float-sign 1.0 sb-ext:single-float-negative-infinity) ; => #.SINGLE-FLOAT-POSITIVE-INFINITY
在 C++ 里,该函数对应于 std::copysignf
。
首先,我们试着通过 Emacs 的 Sly 查找这个函数的定义,SBCL 给出了以下结果:
/usr/share/sbcl-source/src/code/float.lisp
(DEFUN FLOAT-SIGN)
/usr/share/sbcl-source/src/compiler/float-tran.lisp
(:DEFTRANSFORM FLOAT-SIGN (DOUBLE-FLOAT &OPTIONAL DOUBLE-FLOAT) "optimize")
(:DEFTRANSFORM FLOAT-SIGN (SINGLE-FLOAT &OPTIONAL SINGLE-FLOAT) "optimize")
/usr/share/sbcl-source/src/compiler/fndb.lisp
(:DEFOPTIMIZER FLOAT-SIGN DERIVE-TYPE)
(DECLAIM FLOAT-SIGN DEFKNOWN)
为什么一个函数会有多处定义呢?这里其实可以类比一下 CLOS 的 DEFGENERIC
与 DEFMETHOD
的关系,前者定义声明,后者定义实现,并且一条声明根据规则可以实际调用不同的实现。在 SBCL 里,对于 Common Lisp 的内置函数,一旦被声明为 DEFKNOWN
, SBCL 会认为这个函数可以被优化,并可能会提供多种编译方案:
- 如果对应的函数有使用
DEFINE-VOP
定义的虚拟操作符存在且参数与返回值满足条件,则在函数的调用处进行寄存器分配并生成汇编代码段,将其嵌入到程序中。在 SBCL 里,这种生成汇编代码段的对象被称为虚拟操作符 (Virtual OPerator) ,它可以被看作是一种底层的函数,其主体是带有寄存器、参数/返回值类型的约束与分配规则的汇编代码生成器。如+
在 C/C++ 里的运算符,在 Common Lisp 中虽然是函数,但它们对于不同的 Lisp 类型(如fixnum
,single-float
), SBCL 也中具有不同的 VOP 与之对应,使得两个fixnum
相加就是一条ADD
指令,提高了效率。对于float-sign
而言,如果传入了两个参数均为single-float
, SBCL 实际上会调用这个 VOP :(define-vop (single-float-copysign) (:translate single-float-copysign) (:args (x :scs (descriptor-reg)) (y :scs (descriptor-reg))) (:arg-types single-float single-float) (:results (res :scs (descriptor-reg))) (:temporary (:sc unsigned-reg :from (:argument 0)) temp) (:policy :fast-safe) (:generator 3 (move temp x) (move res y) (inst shl res 1) ; discard result's sign bit (inst shl temp 1) ; copy X's sign bit into the carry flag (inst rcr res 1)))
DEFINE-VOP
的基本语法稍后会提到,这里大家可以将其看作一个黑箱,它能够将第一个参数的浮点数符号复制给传入的第二个浮点数并返回。 - 如果对应的函数有使用
DEFTRANSFORM
定义的变换规则存在且满足变换条件(通常包括参数类型、是否可折叠为常量、是否为指定的平台等), SBCL 会使用这个变换规则,将函数的调用变换为一个或多个 VOP 的组合。还是以float-sign
为例, SBCL 里存在这样一个DEFTRANSFORM
:
其实这里稍微熟悉 Lisp 语法的大概都能看出这里是什么意思了,如果传入了两个参数均为(deftransform float-sign ((float &optional float2) (single-float &optional single-float) *) (if (vop-existsp :translate single-float-copysign) (if float2 `(single-float-copysign float float2) `(single-float-sign float)) (if float2 (let ((temp (gensym))) `(let ((,temp (abs float2))) (if (minusp (single-float-bits float)) (- ,temp) ,temp))) '(if (minusp (single-float-bits float)) $-1f0 $1f0))))
single-float
类型 ,并且有为平台定义single-float-copysign
这个 VOP ,那么将函数调用变换为上面提到的single-float-copysign
。 - 如果对应的函数参数类型是未知的、或者不满足所有 VOP 与变换规则的条件,此时 SBCL 就会使用
DEFUN
定义的备用 (Fallback) 函数,它们的地位与用户定义的函数是相当的,调用时不会进行寄存器级别的优化,也就是在函数的调用位置会真的产生了一次函数的调用,效率相对是比较低的。此时,在大部分情况下 SBCL 会给出优化提示,使得对应的 VOP 或变换规则能被应用。(defun float-sign (float1 &optional (float2 (float 1 float1))) "Return a floating-point number that has the same sign as FLOAT1 and, if FLOAT2 is given, has the same absolute value as FLOAT2." (declare (float float1 float2) (explicit-check)) (* (if (etypecase float1 (single-float (minusp (single-float-bits float1))) ;; If 64-bits words, use all the bits. No need to right-shift them. (double-float (minusp #+64-bit (double-float-bits float1) #-64-bit (double-float-high-bits float1))) #+long-float (long-float (minusp (long-float-exp-bits float1)))) (float -1 float1) (float 1 float1)) (abs float2)))
SBCL 引入这样优化机制的好处也是显而易见的:
- 不同指令集的 CPU 显然不可能原生提供相同的功能集合,因此定义的 VOP 的数量与内容一定是与 CPU 架构相关的。如在 x86 上,
POPCNT
指令可以直接计算一个二进制数中1
的数量,使得 Common Lisp 的logcount
函数在 SBCL 上可以被编译为一条汇编指令,但这个在 ARM 上是无法做到的。 - 在移植 SBCL 到不同指令集的 CPU 时,只需要定义少量的 VOP (基本上就是 C/C++ 里的那些原生运算符对应的汇编表示),即可使得 SBCL 以较低的效率(其余未被定义的 VOP 与变换规则全部变成了函数的调用)运行在这个新的平台上。
所以这里大家应该就能明白为什么给 SBCL 加上类型声明能够显著提高程序的执行效率了,添加的类型声明配合 SBCL 类型推断器的辅助,可以使得更多的 VOP 或变换规则被应用,使得用户定义的函数编译后输出高效的机器码。
DEFKNOWN
与 DEFINE-VOP
宏
回到 float-sign
中来, 通过其 single-float
版本的变换规则我们可以得知,传入两个参数均为 single-float
时,调用的实际上是 single-float-copysign
这个函数,那我们先来看一下 single-float-copysign
的 DEFKNOWN
定义:
(defknown single-float-copysign (single-float single-float)
single-float (movable foldable flushable))
其中按照顺序:
-
single-float-copysign
就是其对应的函数名称,在 x86-64 平台上, SBCL 没有为其定义备用函数,因为float-sign
的变换规则保证了这个函数传入的一定是单精度浮点,其备用函数是这样定义的:
有人可能会说这不是无限递归了吗?其实不然,(defun single-float-copysign (float float2) (single-float-copysign float float2))
single-float-copysign
这个函数在编译时,函数体内的single-float-copysign
就会被定义好的 VOP 取代,因此该函数的反编译结果如下:; disassembly for SINGLE-FLOAT-COPYSIGN ; Size: 23 bytes. Origin: #x53449882 ; SINGLE-FLOAT-COPYSIGN ; 82: 488BC6 MOV RAX, RSI ; 85: 488BD7 MOV RDX, RDI ; 88: 48D1E2 SHL RDX, 1 ; 8B: 48D1E0 SHL RAX, 1 ; 8E: 48D1DA RCR RDX, 1 ; 91: 488BE5 MOV RSP, RBP ; 94: F8 CLC ; 95: 5D POP RBP ; 96: C3 RET ; 97: CC10 INT3 16 ; Invalid argument count trap
-
(single-float single-float)
定义了传入参数的类型,只有实际(断言的)类型符合这个类型要求,为这个DEFKNOWN
定义的 VOP 与变换规则才可能被应用。 -
single-float
定义了为DEFKNOWN
定义的 VOP 与规则的返回值类型。 -
(movable foldable flushable)
定义了允许编译器应用的优化,其中:-
movable
: 允许指令重排。 -
foldable
: 允许常量折叠以减少相同的指令执行。 -
flushable
: 允许删除不必要的指令以减小代码的体积并提高其效率。
-
我们继续看一下对应的 VOP 定义:
(define-vop (single-float-copysign)
(:translate single-float-copysign)
(:args (x :scs (descriptor-reg))
(y :scs (descriptor-reg)))
(:arg-types single-float single-float)
(:results (res :scs (descriptor-reg)))
(:temporary (:sc unsigned-reg :from (:argument 0)) temp)
(:policy :fast-safe)
(:generator 3
(move temp x)
(move res y)
(inst shl res 1) ; discard result's sign bit
(inst shl temp 1) ; copy X's sign bit into the carry flag
(inst rcr res 1)))
其中按照顺序:
-
single-float-copysign
是这个 VOP 的名称,不同的 VOP 可以继承,但必须具有不同名称,否则会被视为 VOP 重定义。 -
:translate
指定要转换的目标函数,也就是之前DEFKNOWN
的名称。 -
:args
指定传入的参数与需要分配的寄存器,使用:scs
参数指定一个或多个寄存器类型,使用:target
指定目标寄存器。 -
:arg-types
指定参数类型,这里的类型可以是DEFKNOWN
参数里的子类型。 -
:results
指定为返回值分配寄存器,参数与:args
类似。注意由于 Common Lisp 支持多值返回,这里与:args
一样也可以分配多个寄存器。 -
:result-types
这里没出现,但还是提一下,与:arg-types
是类似的。 -
:temporary
用于指定中间寄存器,可以使用:sc
指定寄存器的类型,使用:from
与:to
指定寄存器中的值来源与去向。 -
:generator
的第一个参数指定开销,有多个 VOP 满足条件时, SBCL 选择开销最小的 VOP 使用,其余参数是汇编代码的生成器:(move temp x) (move res y) (inst shl res 1) (inst shl temp 1) (inst rcr res 1)
以
inst
后的汇编代码将被原封不动地释放,而move
可以让 SBCL 根据寄存器类型自动选用合适的移动指令,这里等价于(inst mov ...)
。根据 IEEE 754 浮点数标准:
1位 8位 23位 .---.-----------------------.--------------------------------------------------------------. +---+-----------------------+--------------------------------------------------------------+ | S | e | f | +---+-----------------------+--------------------------------------------------------------+
这一段汇编代码的作用是将第一个参数的符号位复制给第二个参数并返回。
示例
优化 SBCL 的 float-sign
我们可以尝试优化一下 SBCL 的 single-float-copysign
。这里先给出在一般场景下调用 float-sign
的测试函数:
(defun test-float-sign (x y)
(declare (optimize (speed 3)
(debug 0)
(safety 0))
(type single-float x y)
(values single-float))
(+ (float-sign x y) (float-sign y x)))
然后我们对其汇编代码进行分析:
; disassembly for TEST-FLOAT-SIGN
; Size: 69 bytes. Origin: #x535D7AE6 ; TEST-FLOAT-SIGN
; AE6: 488BC2 MOV RAX, RDX
; AE9: 488BCF MOV RCX, RDI
; AEC: 48D1E1 SHL RCX, 1
; AEF: 48D1E0 SHL RAX, 1
; AF2: 48D1D9 RCR RCX, 1
; AF5: 66480F6EC9 MOVQ XMM1, RCX
; AFA: 0FC6C9FD SHUFPS XMM1, XMM1, #4r3331
; AFE: 488BC7 MOV RAX, RDI
; B01: 488BCA MOV RCX, RDX
; B04: 48D1E1 SHL RCX, 1
; B07: 48D1E0 SHL RAX, 1
; B0A: 48D1D9 RCR RCX, 1
; B0D: 66480F6ED1 MOVQ XMM2, RCX
; B12: 0FC6D2FD SHUFPS XMM2, XMM2, #4r3331
; B16: F30F58D1 ADDSS XMM2, XMM1
; B1A: 660F7ED2 MOVD EDX, XMM2
; B1E: 48C1E220 SHL RDX, 32
; B22: 80CA19 OR DL, 25
; B25: 488BE5 MOV RSP, RBP
; B28: F8 CLC
; B29: 5D POP RBP
; B2A: C3 RET
很显然,这里调用的就是 single-float-copysign
这个 VOP ,不过大家发现问题了吗?由于 SBCL 的 single-float-copysign
这个 VOP 是针对描述符寄存器的,使用的是整数的位运算,而浮点运算使用的是 SSE 浮点数寄存器 XMM
,所以会产生不必要的寄存器移动与 CPU 状态切换,这里显然是比较低效的。我们可以为 SBCL 的 single-float-copysign
定义一个基于 SSE 指令集的高效版本:
#+(and sbcl x86-64)
(sb-c:define-vop (single-float-copysign-sse)
(:translate sb-kernel:single-float-copysign)
(:args (xmm3 :scs (sb-vm::single-reg))
(xmm1 :scs (sb-vm::single-reg)))
(:arg-types single-float single-float)
(:temporary (:sc sb-vm::single-reg :target r :to :result) xmm0)
(:temporary (:sc sb-vm::single-reg) xmm2)
(:results (r :scs (sb-vm::single-reg)))
(:result-types single-float)
(:policy :fast-safe)
(:generator 2
(sb-vm::inst movss xmm2 (sb-vm::constantize #x80000000))
(sb-vm::inst movaps xmm0 xmm2)
(sb-vm::inst andnps xmm0 xmm1)
(sb-vm::inst andps xmm2 xmm3)
(sb-vm::inst orps xmm0 xmm2)
(unless (sb-c:location= r xmm0)
(sb-vm::inst movss r xmm0))))
这里通过一些逻辑运算可以将 xmm3
寄存器中符号位与 xmm1
中的非符号位结合,结果存入 xmm0
寄存器中,其中用到的还有两个其他的 SBCL 内置函数:
-
sb-vm::constantize
用于插入一个常量,这里的#x80000000
是二进制下最高位为1
的掩码,用于处理符号位。 -
sb-c:location=
用于判断两个分配后的寄存器的相等性,在这里,如果没法将寄存器xmm0
与返回值寄存器r
分配为同一个寄存器,则需要将xmm0
的内容移动至返回值寄存器r
中。
注意我们定义的 single-float-copysign-sse
中, :generator
后的开销需要小于原来 SBCL 定义的 single-float-copysign
的开销,否则编译器不会选用我们定义的 VOP 。
对这段代码进行编译后,下面还是编译相同函数进行测试:
(defun test-float-sign (x y)
(declare (optimize (speed 3)
(debug 0)
(safety 0))
(type single-float x y)
(values single-float))
(+ (float-sign x y) (float-sign y x)))
反编译后的结果:
; disassembly for TEST-FLOAT-SIGN
; Size: 69 bytes. Origin: #x536BE968 ; TEST-FLOAT-SIGN
; 68: F30F100DC8FFFFFF MOVSS XMM1, [RIP-56] ; [#x536BE938]
; 70: 0F28C1 MOVAPS XMM0, XMM1
; 73: 0F55C3 ANDNPS XMM0, XMM3
; 76: 0F54CA ANDPS XMM1, XMM2
; 79: 0F56C1 ORPS XMM0, XMM1
; 7C: F30F10E0 MOVSS XMM4, XMM0
; 80: F30F100DB0FFFFFF MOVSS XMM1, [RIP-80] ; [#x536BE938]
; 88: 0F28C1 MOVAPS XMM0, XMM1
; 8B: 0F55C2 ANDNPS XMM0, XMM2
; 8E: 0F54CB ANDPS XMM1, XMM3
; 91: 0F56C1 ORPS XMM0, XMM1
; 94: F30F10D0 MOVSS XMM2, XMM0
; 98: F30F58D4 ADDSS XMM2, XMM4
; 9C: 660F7ED2 MOVD EDX, XMM2
; A0: 48C1E220 SHL RDX, 32
; A4: 80CA19 OR DL, 25
; A7: 488BE5 MOV RSP, RBP
; AA: F8 CLC
; AB: 5D POP RBP
; AC: C3 RET
我们来测试一下 float-sign
的正确性:
(float-sign -1.0 22.0) ; => -22.0
(float-sign 3.0 22.0) ; => 22.0
(float-sign -1.0 sb-ext:single-float-positive-infinity) ; => #.SINGLE-FLOAT-NEGATIVE-INFINITY
结果都是正确的。优化前后汇编代码减少了两行,但实际考虑到 CPU 的状态切换,产生的性能差距应该会更大一些。下面我们将这个函数内联,在循环里实测一下性能:
(defun performance-test ()
(declare (optimize (speed 3)
(debug 0)
(safety 0)))
(trivial-benchmark:with-timing (10)
(loop :for x :of-type single-float := 1.0 :then (+ x (test-float-sign -1.0 x))
:repeat 100000000
:finally (return x))))
(performance-test)
使用 trivial-benchmark
对循环进行 10 次采样的结果如下:
- SBCL 原始版本
- SAMPLES TOTAL MINIMUM MAXIMUM MEDIAN AVERAGE DEVIATION REAL-TIME 10 4.873373 0.486669 0.493337 0.486671 0.487337 0.002 RUN-TIME 10 4.86349 0.484148 0.490104 0.485853 0.486349 0.001473 USER-RUN-TIME 10 4.856902 0.480859 0.490097 0.485855 0.48569 0.0024 SYSTEM-RUN-TIME 10 0.006596 0 0.003302 0 0.00066 0.001318 PAGE-FAULTS 10 0 0 0 0 0 0.0 GC-RUN-TIME 10 0 0 0 0 0 0.0 BYTES-CONSED 10 0 0 0 0 0 0.0 EVAL-CALLS 10 0 0 0 0 0 0.0
- 自定义的 SSE 指令集版本
- SAMPLES TOTAL MINIMUM MAXIMUM MEDIAN AVERAGE DEVIATION REAL-TIME 10 3.166692 0.313335 0.326669 0.316669 0.316669 0.003651 RUN-TIME 10 3.163733 0.31455 0.327258 0.314947 0.316373 0.003683 USER-RUN-TIME 10 3.163736 0.314551 0.327247 0.314949 0.316374 0.003679 SYSTEM-RUN-TIME 10 0.000009 0 0.000009 0 0.000001 0.000003 PAGE-FAULTS 10 0 0 0 0 0 0.0 GC-RUN-TIME 10 0 0 0 0 0 0.0 BYTES-CONSED 10 0 0 0 0 0 0.0 EVAL-CALLS 10 0 0 0 0 0 0.0
使用 SSE 指令集的版本快了 35% ,是个比较可观的性能提升。
实现 CPUID
CPUID 是 x86 架构中的一条指令,它允许软件访问 CPU 的信息和功能。该指令可用于检索有关 CPU 的各种信息,例如型号、功能、支持的指令集以及操作系统和应用程序可以使用的特性。
要使用 CPUID 指令,我们需要将所需信息的编号放入 EAX
寄存器,然后执行 CPUID
指令。执行后, EAX
、 EBX
、 ECX
和 EDX
寄存器将包含所请求的信息,这在高级语言里不通过内联汇编或一些内置函数 (Intrinsics Function) 是无法做到的。下面我们就在 SBCL 里实现一下获取 CPUID
的返回的信息,为了方便起见,我们定义一个包用于导入 SBCL 的一些内部符号:
(defpackage sb-asm-test
(:use #:cl #:sb-c #:sb-vm)
(:nicknames #:asm-bench)
(:import-from #:sb-vm #:inst #:function-raw-address
#:any-reg #:unsigned-reg #:unsigned-num #:positive-fixnum #:tagged-num .
#+x86-64
(#:rax-offset #:rcx-offset #:rdx-offset #:rbx-offset #:rsp-offset #:rbp-offset #:rsi-offset #:rdi-offset
#:r8-offset #:r9-offset #:r10-offset #:r11-offset #:r12-offset #:r13-offset #:r14-offset #:r15-offset))
(:import-from #:sb-x86-64-asm #:ea))
先给出 DEFKNOWN
定义:
(in-package #:sb-asm-test)
(defknown cpuid ((unsigned-byte 32))
(values (unsigned-byte 32) (unsigned-byte 32) (unsigned-byte 32) (unsigned-byte 32))
(flushable movable)) ; 不能将这个函数标注为 `foldable'
这里需要注意一下,由于 CPUID
在不同的机器上的结果可能不一样,所以我们不应该让编译器在传入常量时优化掉这条指令。下面是 DEFINE-VOP
的定义:
(in-package #:sb-asm-test)
(define-vop (cpuid)
(:policy :fast-safe)
(:translate cpuid)
(:args (arg-rax :scs (unsigned-reg) :target rax))
(:arg-types unsigned-num)
(:temporary (:sc unsigned-reg :offset rax-offset) rax)
(:temporary (:sc unsigned-reg :offset rbx-offset) rbx)
(:temporary (:sc unsigned-reg :offset rcx-offset) rcx)
(:temporary (:sc unsigned-reg :offset rdx-offset) rdx)
(:results (res-1 :scs (unsigned-reg)) (res-2 :scs (unsigned-reg))
(res-3 :scs (unsigned-reg)) (res-4 :scs (unsigned-reg)))
(:result-types unsigned-num unsigned-num unsigned-num unsigned-num)
(:generator
4
(unless (location= arg-rax rax)
(move rax arg-rax))
(inst cpuid)
(loop :for reg :in (list rax rbx rcx rdx)
:for res :in (list res-1 res-2 res-3 res-4)
:unless (location= reg res)
:do (move res reg))))
最后我们需要定义一个同名函数作为入口。前面也提到了,函数体在编译的过程中会展开为我们上面编写的 cpuid
这个 VOP ,所以不用担心无限递归的问题,但需要注意类型声明,使得这个 VOP 能被应用:
(in-package #:sb-asm-test)
(defun cpuid (eax)
(declare (type (unsigned-byte 32) eax)
(optimize (speed 3)
(safety 0)
(debug 0)))
(cpuid eax))
下面我们反编译这个函数看看:
; disassembly for CPUID
; Size: 61 bytes. Origin: #x536F14FA ; CPUID
; 4FA: 48D1FE SAR RSI, 1
; 4FD: 488BC6 MOV RAX, RSI
; 500: 0FA2 CPUID
; 502: 4C8BC0 MOV R8, RAX
; 505: 488BFB MOV RDI, RBX
; 508: 488BF1 MOV RSI, RCX
; 50B: 4C8BCA MOV R9, RDX
; 50E: 49D1E0 SHL R8, 1
; 511: 48D1E7 SHL RDI, 1
; 514: 48D1E6 SHL RSI, 1
; 517: 49D1E1 SHL R9, 1
; 51A: 498BD0 MOV RDX, R8
; 51D: 4C894DF0 MOV [RBP-16], R9
; 521: 488D5D10 LEA RBX, [RBP+16]
; 525: B908000000 MOV ECX, 8
; 52A: F9 STC
; 52B: 488D65F0 LEA RSP, [RBP-16]
; 52F: 488B6D00 MOV RBP, [RBP]
; 533: FF73F8 PUSH QWORD PTR [RBX-8]
; 536: C3 RET
可以看到, CPUID
这条汇编指令成功地出现在我们的 cpuid
这个函数体里。值得一提的是由于 cpuid
的参数与返回值是 32 位无符号整数,可以被 fixnum
容纳,因此 SBCL 自动将参数与返回值均视为 fixnum
而进行了移位操作,这意味着对于整数、浮点数这些,我们通常不需要考虑 VOP 的参数、返回值与 Lisp 对象的转换问题,像 (unsigned-byte 64)
这样无法被 fixnum
容纳的类型,
SBCL 也会进行自动的拆装箱操作。
下面我们可以实际地运用一下我们编写的这个 cpuid
函数,获取 CPU 的制造商信息:
(in-package #:sb-asm-test)
(defun cpu-vendor ()
(declare (optimize (speed 3)
(debug 0)
(safety 0)))
(multiple-value-bind (eax ebx ecx edx) (cpuid 0)
(declare (ignore eax))
(loop :for reg :of-type (unsigned-byte 32) :in (list ebx edx ecx)
:nconc (loop :for i :below 4
:collect (code-char (ldb (byte 8 (* i 8)) reg)))
:into result
:finally (return (coerce result 'string)))))
(cpu-vendor) ; => "GenuineIntel"
总结
SBCL 中的 VOP 可以看作是一般的 Lisp 程序与汇编程序的桥梁,当 Lisp 程序中函数的调用类型能够确定且满足条件时, SBCL 会尝试使用 VOP 或一系列变换规则对其进行优化,利用这一点,我们可以比较容易地在 SBCL 里内联汇编,实现提高性能或进行一些硬件操作。作为动态语言实现的 SBCL (前身为 CMUCL ),其具有极高的上限(指动态性),也具有极高的下限(指底层编程的能力),加上 Lisp 的表达能力与交互性, 世界上很难有第二门语言能在这几个方面 同时 与之媲美的,但可惜的是 SBCL 没有为其底层编程的能力对外开放一套较为稳定的 API 并且缺乏对应的文档,因此本文中的所有代码仅保证在 x86-64 的 SBCL 2.2.11 上能够正常运行,希望以后 SBCL 开发者能在这方面下一点功夫。这里也安利给学习汇编或做底层开发的道友,不妨可以试一下 SBCL ,秒编译秒运行的体验确实相当地棒!