示例代码:
(setq x 60)
(setq y 40)
(+ x y)
使用 Lisp 解释器执行
在上面的基于解释器的 Lisp 中,将是 Lisp 数据,解释器查看每个表单并运行评估器。由于它运行的是 Lisp 数据结构,所以每次看到上面的代码时都会这样做
- 得到第一个表格
- 我们有一个表达
- 它是一个 SETQ 特殊形式
- 评估 60,结果为 60
- 查找变量 x 的位置
- 将变量 x 设置为 60
- 获取下一个表格......
- 我们有一个函数调用 +
- 评估 x -> 60
- 评估 y -> 40
- 用 60 和 40 -> 100 调用函数 + ...
现在+
是一些代码,它实际上找出了要做什么。Lisp 通常具有不同的数字类型,并且(几乎)没有处理器支持所有这些:fixnums、bignums、ratio、complex、float ......所以+
函数需要找出参数的类型以及它可以做些什么来添加他们。
使用 Lisp 编译器执行
编译器将简单地发出机器代码,机器代码将执行操作。机器代码将完成解释器所做的一切:检查变量、检查类型、检查参数数量、调用函数……
如果您运行机器代码,它会快得多,因为不需要查看和解释 Lisp 表达式。解释器需要解码每个表达式。编译器已经完成了。
它仍然比某些 C 代码慢,因为编译器不一定知道类型,而只是发出完全安全和灵活的代码。
所以这个编译的 Lisp 代码比运行原始 Lisp 代码的解释器快得多。
使用优化的 Lisp 编译器
有时它不够快。然后你需要一个更好的编译器并告诉 Lisp 编译器它应该在编译中投入更多的工作并创建优化的代码。
Lisp 编译器可能知道参数和变量的类型。然后,您可以告诉编译器忽略运行时检查。编译器还可以假设+
始终是相同的操作。所以它可能会内联必要的代码。由于它知道类型,它可能只为这些类型生成代码:整数加法。
但是 Lisp 的语义仍然不同于 C 或机器操作。A+
不仅处理各种数字类型,它还会自动从小整数 (fixnums) 切换到大整数 (bignums) 或在某些类型的溢出时发出错误信号。您还可以告诉编译器忽略它,而只使用本机整数加法。然后你的代码会更快——但不像普通代码那样安全和灵活。
这是使用 64 位 LispWorks 实现的完全优化代码的示例。它使用类型声明、内联声明和优化指令。你看我们必须告诉编译器一点:
(defun foo-opt (x y)
(declare (optimize (speed 3) (safety 0) (debug 0) (fixnum-safety 0))
(inline +))
(declare (fixnum x y))
(the fixnum (+ x y)))
然后代码(64 位 Intel 机器代码)非常小,并且针对我们告诉编译器的内容进行了优化:
0: 4157 push r15
2: 55 push rbp
3: 4889E5 moveq rbp, rsp
6: 4989DF moveq r15, rbx
9: 4803FE addq rdi, rsi
12: B901000000 move ecx, 1
17: 4889EC moveq rsp, rbp
20: 5D pop rbp
21: 415F pop r15
23: C3 ret
24: 90 nop
25: 90 nop
26: 90 nop
27: 90 nop
但请记住,上面的代码所做的事情与解释器或安全代码所做的事情不同:
- 它只计算fixnums
- 它会默默地溢出
- 结果也是一个fixnum
- 它没有错误检查
- 它不适用于其他数字数据类型
现在未优化的代码:
0: 49396275 cmpq [r10+75], rsp
4: 7741 ja L2
6: 4883F902 cmpq rcx, 2
10: 753B jne L2
12: 4157 push r15
14: 55 push rbp
15: 4889E5 moveq rbp, rsp
18: 4989DF moveq r15, rbx
21: 4989F9 moveq r9, rdi
24: 4C0BCE orq r9, rsi
27: 41F6C107 testb r9b, 7
31: 7517 jne L1
33: 4989F9 moveq r9, rdi
36: 4C03CE addq r9, rsi
39: 700F jo L1
41: B901000000 move ecx, 1
46: 4C89CF moveq rdi, r9
49: 4889EC moveq rsp, rbp
52: 5D pop rbp
53: 415F pop r15
55: C3 ret
L1: 56: 4889EC moveq rsp, rbp
59: 5D pop rbp
60: 415F pop r15
62: 498B9E070E0000 moveq rbx, [r14+E07] ; SYSTEM::*%+$ANY-CODE
69: FFE3 jmp rbx
L2: 71: 41FFA6E7020000 jmp [r14+2E7] ; SYSTEM::*%WRONG-NUMBER-OF-ARGUMENTS-STUB
...
您可以看到它调用了一个库例程来进行添加。这段代码完成了解释器会做的所有事情。但它不需要解释 Lisp 源代码。它已经编译成相应的机器指令。
为什么编译的 Lisp 代码很快(呃)?
那么,为什么编译的 Lisp 代码很快呢?两种情况:
作为一名 Lisp 程序员,您大部分时间都希望使用未经优化但已编译的 Lisp 代码。它足够快并且提供了很多舒适。
不同的执行模式提供选择
作为 Lisp 程序员,我们有以下选择:
- 解释代码:慢,但最容易调试
- 编译代码:运行时速度快,编译速度快,编译器检查很多,调试难度稍大,完全动态
- 优化的代码:运行时非常快,运行时可能不安全,各种优化的大量编译噪音,编译速度慢
通常我们只优化那些需要速度的代码部分。
请记住,在很多情况下,即使是好的 Lisp 编译器也无法创造奇迹。一个完全通用的面向对象程序(使用 Common Lisp 对象系统)几乎总是会有一些开销(基于运行时类的调度,...)。
动态类型和动态不一样
另请注意,动态类型和动态是编程语言的不同属性:
Lisp 是动态类型的,因为类型检查是在运行时完成的,并且默认情况下可以将变量设置为各种对象。为此,Lisp 还需要附加到数据对象本身的类型。
Lisp 是动态的,因为 Lisp 编程语言和程序本身都可以在运行时更改:我们可以添加、更改和删除函数,我们可以添加、更改或删除语法结构,我们可以添加、更改或删除数据类型(记录、类,...),我们可以通过各种方式改变 Lisp 的表面语法,等等。Lisp 也是动态类型的,以提供其中一些特性。
用户界面:编译和反汇编
ANSI Common Lisp 提供