2 Star 13 Fork 3

advanceflow/Elisp

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
17-字节编译.org 24.76 KB
一键复制 编辑 原始数据 按行查看 历史
advanceflow 提交于 2022-05-28 10:12 . finish s2

17 字节编译

Emacs Lisp 有一个编译器,它将用 Lisp 编写的函数翻译成一种称为字节码的特殊表示形式,可以更有效地执行。 编译器将 Lisp 函数定义替换为字节码。 当调用字节码函数时,其定义由字节码解释器评估。

因为字节编译代码由字节代码解释器评估,而不是由机器硬件直接执行(就像真正的编译代码那样),字节代码完全可以在机器之间传输而无需重新编译。 但是,它不如真正的编译代码快。

一般来说,任何版本的 Emacs 都可以运行由最近早期版本的 Emacs 生成的字节编译代码,但反之则不然。

如果您不希望编译 Lisp 文件,请在其中放置一个用于无字节编译的文件局部变量绑定,如下所示:

;; -*-no-byte-compile: t; -*-

17.1 字节编译代码的性能

字节编译函数不如用 C 编写的原始函数高效,但运行速度比用 Lisp 编写的版本快得多。 这是一个例子:

(defun silly-loop (n)
  "Return the time, in seconds, to run N iterations of a loop."
  (let ((t1 (float-time)))
    (while (> (setq n (1- n)) 0))
    (- (float-time) t1)))
⇒ silly-loop


(silly-loop 50000000)
⇒ 10.235304117202759


(byte-compile 'silly-loop)
⇒ [Compiled code not shown]


(silly-loop 50000000)
⇒ 3.705854892730713

在此示例中,解释代码需要 10 秒才能运行,而字节编译代码需要不到 4 秒。 这些结果具有代表性,但实际结果可能会有所不同。

17.2 字节编译函数

您可以使用字节编译函数对单个函数或宏定义进行字节编译。 您可以使用 byte-compile-file 编译整个文件,也可以使用 byte-recompile-directory 或 batch-byte-compile 编译多个文件。

有时,字节编译器会产生警告和/或错误消息(有关详细信息,请参阅编译器错误)。 这些消息通常记录在一个名为 Compile-Log 的缓冲区中,该缓冲区使用编译模式。 请参阅 GNU Emacs 手册中的编译模式。 但是,如果变量 byte-compile-debug 不为零,则错误消息将作为 Lisp 错误发出信号(请参阅错误)。

在您打算进行字节编译的文件中编写宏调用时要小心。 由于宏调用在编译时会扩展,因此需要将宏加载到 Emacs 中,否则字节编译器将无法执行正确的操作。 处理此问题的常用方法是使用指定包含所需宏定义的文件的要求表单(请参阅功能)。 通常,字节编译器不会评估它正在编译的代码,但它会通过加载指定的库来专门处理需求表单。 为了避免在有人运行已编译的程序时加载宏定义文件,请在 require 调用周围编写 eval-when-compile(请参阅编译期间的评估)。 有关更多详细信息,请参阅宏和字节编译。

内联(defsubst)函数不那么麻烦; 如果您在知道其定义之前编译对此类函数的调用,则该调用仍然可以正常工作,只是运行速度较慢。

Function: byte-compile symbol ¶

该函数对符号的函数定义进行字节编译,将之前的定义替换为编译后的定义。 symbol的函数定义必须是函数的实际代码; 字节编译不处理函数间接。 返回值是字节码函数对象,它是符号的编译定义(参见字节码函数对象)。



     (defun factorial (integer)
	"Compute factorial of INTEGER."
	(if (= 1 integer) 1
	  (* integer (factorial (1- integer)))))
     ⇒ factorial


     (byte-compile 'factorial)
     ⇒
     #[(integer)
	"^H\301U\203^H^@\301\207\302^H\303^HS!\"\207"
	[integer 1 * factorial]
	4 "Compute factorial of INTEGER."]

如果 symbol 的定义是一个字节码函数对象,byte-compile 什么都不做并且返回 nil。 它不会再次编译符号的定义,因为原始(未编译的)代码已经在符号的函数单元中被字节编译代码替换。

byte-compile 的参数也可以是 lambda 表达式。 在这种情况下,该函数会返回相应的编译代码,但不会将其存储在任何地方。

Command: compile-defun &optional arg ¶

此命令读取包含点的 defun,对其进行编译并评估结果。 如果在实际上是函数定义的 defun 上使用它,效果是安装该函数的编译版本。

compile-defun 通常在 echo 区域显示计算结果,但如果 arg 不为零,它会将结果插入到它编译的表单之后的当前缓冲区中。

Command: byte-compile-file filename ¶

该函数将名为 filename 的 Lisp 代码文件编译成字节码文件。 输出文件的名称是通过将“.el”后缀更改为“.elc”来获得的; 如果文件名不以“.el”结尾,则将“.elc”添加到文件名的末尾。

编译通过一次读取一种形式的输入文件来工作。 如果是函数或宏的定义,则写出编译后的函数或宏定义。 其他形式一起批处理,然后每个批处理都被编译和编写,以便在读取文件时执行其编译的代码。 读取输入文件时,所有注释都将被丢弃。

如果没有错误,此命令返回 t,否则返回 nil。 当以交互方式调用时,它会提示输入文件名。

  $ ls -l push*
  -rw-r--r-- 1 lewis lewis 791 Oct  5 20:31 push.el


  (byte-compile-file "~/emacs/push.el")
	   ⇒ t


  $ ls -l push*
  -rw-r--r-- 1 lewis lewis 791 Oct  5 20:31 push.el
  -rw-rw-rw- 1 lewis lewis 638 Oct  8 20:25 push.elc
Command: byte-recompile-directory directory &optional flag force follow-symlinks ¶

此命令重新编译目录(或其子目录)中需要重新编译的每个“.el”文件。 如果“.elc”文件存在但比“.el”文件旧,则需要重新编译文件。

当一个 ‘.el’ 文件没有对应的 ‘.elc’ 文件时,flag 说明要做什么。 如果为 nil,此命令将忽略这些文件。 如果 flag 为 0,则编译它们。 如果它既不是 nil 也不是 0,它询问用户是否编译每个这样的文件,并询问每个子目录。

交互地,字节重新编译目录提示目录,标志是前缀参数。

如果 force 不为零,则此命令重新编译每个具有 ‘.elc’ 文件的 ‘.el’ 文件。

此命令通常不会编译符号链接的“.el”文件。 如果可选的 follow-symlink 参数不为 nil,则符号链接的 ‘.el’ 也将被编译。

返回的值是不可预测的。

Function: batch-byte-compile &optional noforce ¶

此函数在命令行上指定的文件上运行 byte-compile-file。 该函数只能在 Emacs 的批处理执行中使用,因为它会在完成时杀死 Emacs。 一个文件中的错误不会阻止后续文件的处理,但不会为其生成输出文件,并且 Emacs 进程将以非零状态码终止。

如果 noforce 不为零,则此函数不会重新编译具有最新 ‘.elc’ 文件的文件。

$ emacs -batch -f batch-byte-compile *.el

17.3 文档字符串和编译

当 Emacs 从字节编译文件加载函数和变量时,它通常不会将它们的文档字符串加载到内存中。 每个文档字符串仅在需要时从字节编译文件中动态加载。 这样可以节省内存,并通过跳过文档字符串的处理来加快加载速度。

此功能有一个缺点:如果您删除、移动或更改已编译的文件(例如通过编译新版本),Emacs 可能不再能够访问先前加载的函数或变量的文档字符串。 此类问题通常仅在您自己构建 Emacs 并且碰巧编辑和/或重新编译 Lisp 源文件时才会出现。 要解决它,只需在重新编译后重新加载每个文件。

对于每个字节编译文件,在编译时确定从字节编译文件动态加载文档字符串。 可以通过选项 byte-compile-dynamic-docstrings 禁用它。

User Option: byte-compile-dynamic-docstrings ¶

如果这是非零,字节编译器生成为动态加载文档字符串而设置的编译文件。

要禁用特定文件的动态加载功能,请在其标题行中将此选项设置为 nil(请参阅 GNU Emacs 手册中的文件中的局部变量),如下所示:

-*-byte-compile-dynamic-docstrings: nil;-*-

这主要在您希望更改文件时很有用,并且您希望已经加载它的 Emacs 会话在文件更改时继续工作。

在内部,文档字符串的动态加载是通过使用特殊的 Lisp 阅读器结构“#@count”编写编译文件来完成的。 此构造跳过下一个 count 字符。 它还使用代表此文件名称的“#$”构造作为字符串。 不要在 Lisp 源文件中使用这些结构; 它们的设计目的不是让阅读文件的人清楚。

17.4 单个函数的动态加载

编译文件时,您可以选择启用动态函数加载功能(也称为延迟加载)。 使用动态函数加载,加载文件不会完全读取文件中的函数定义。 相反,每个函数定义都包含一个引用文件的占位符。 第一次调用每个函数时,它会从文件中读取完整的定义,以替换占位符。

动态函数加载的优点是加载文件应该变得更快。 对于包含许多单独的用户可调用函数的文件来说,这是一件好事,如果使用其中一个并不意味着您可能还会使用其余的。 提供许多键盘命令的专用模式通常具有这种使用模式:用户可以调用该模式,但只使用它提供的少数命令。

动态加载功能有一定的缺点:

如果在加载后删除或移动已编译的文件,Emacs 将无法再加载其余尚未加载的函数定义。 如果您更改编译的文件(例如通过编译新版本),那么尝试加载任何尚未加载的函数通常会产生无意义的结果。

在安装 Emacs 文件的正常情况下,这些问题永远不会发生。 但是它们很可能发生在您正在更改的 Lisp 文件中。 防止这些问题的最简单方法是在每次重新编译后立即重新加载新的编译文件。

经验表明,使用动态函数加载提供了难以衡量的好处,因此自 Emacs 27.1 起,此功能已被弃用。

如果变量 byte-compile-dynamic 在编译时不为零,则字节编译器使用动态函数加载功能。 不要全局设置此变量,因为动态加载仅适用于某些文件。 相反,为具有文件局部变量绑定的特定源文件启用该功能。 例如,您可以通过在源文件的第一行写入以下文本来做到这一点:

-*-byte-compile-dynamic: t;-*-
Variable: byte-compile-dynamic ¶

如果这是非零,字节编译器生成为动态函数加载设置的编译文件。

Function: fetch-bytecode function ¶

如果 function 是一个字节码函数对象,如果它还没有完全加载,这将立即完成从其字节编译文件中加载函数的字节码。 否则,它什么也不做。 它总是返回函数。

17.5 编译期间的评估

这些功能允许您编写在程序编译期间进行评估的代码。

Macro: eval-and-compile body… ¶

当您编译包含代码和运行它时(无论是否编译),此表单都标记要评估的主体。

您可以通过将正文放在单独的文件中并使用 require 引用该文件来获得类似的结果。 当体型较大时,该方法更可取。 实际上 require 是自动 eval-and-compile ,在编译和执行时都会加载包。

自动加载也是有效的评估和编译。 它在编译时被识别,因此使用这样的函数不会产生“未知被定义”的警告。

eval-and-compile 的大多数使用都相当复杂。

如果一个宏有一个辅助函数来构建它的结果,并且该宏在本地和包外部都使用,那么 eval-and-compile 应该用于在编译时获取帮助器,然后在运行时获取帮助器。

如果函数是通过程序定义的(比如 fset),那么 eval-and-compile 可用于在编译时和运行时完成,因此检查对这些函数的调用(以及有关“未知被定义”抑制)。

Macro: eval-when-compile body… ¶

此表单标记要在编译时评估的主体,而不是在加载已编译的程序时。 编译器的评估结果成为一个常量,出现在编译的程序中。 如果您加载源文件,而不是编译它,则正常评估正文。

如果你有一个常量需要一些计算来产生,eval-when-compile 可以在编译时完成。 例如,

     (defvar my-regexp
	(eval-when-compile (regexp-opt '("aaa" "aba" "abb"))))

如果您正在使用另一个包,但只需要其中的宏(字节编译器将扩​​展这些宏),则可以使用 eval-when-compile 加载它以进行编译,但不执行。 例如,

     (eval-when-compile
	(require 'my-macro-package))

同样的事情也适用于本地定义的宏和 defsubst 函数,并且只能在文件中使用。 编译文件需要它们,但在大多数情况下,执行编译文件不需要它们。 例如,

     (eval-when-compile
	(unless (fboundp 'some-new-thing)
	  (defmacro 'some-new-thing ()
	    (compatibility code))))

这通常适用于仅作为与其他 Emacs 版本兼容的后备代码的代码。

Common Lisp 注意:在顶层,eval-when-compile 类似于 Common Lisp 习语(eval-when (compile eval) …)。 在其他地方,Common Lisp ‘#.’ reader 宏(但不是在解释时)更接近 eval-when-compile 所做的。

17.6 编译器错误

来自字节编译的错误和警告消息打印在名为 Compile-Log 的缓冲区中。 这些消息包括标识问题位置的文件名和行号。 用于操作编译器输出的常用 Emacs 命令可用于这些消息。

当错误是由于程序中的无效语法引起的,字节编译器可能会对错误的确切位置感到困惑。 一种调查方法是切换到缓冲区 *Compiler Input*。 (此缓冲区名称以空格开头,因此它不会显示在缓冲区菜单中。)此缓冲区包含正在编译的程序,点显示字节编译器能够读取多远; 错误的原因可能就在附近。 有关定位语法错误的一些提示,请参阅调试无效的 Lisp 语法。

字节编译器发出的常见警告类型是针对已使用但未定义的函数和变量。 此类警告报告文件末尾的行号,而不是使用缺失函数或变量的位置; 要找到这些,您必须手动搜索文件。

如果您确定有关缺少函数或变量的警告消息是不合理的,有几种方法可以抑制它:

您可以通过在 fboundp 测试上对其进行条件化来抑制对函数 func 的特定调用的警告,如下所示:

(if (fboundp 'func) ...(func ...)...)

对 func 的调用必须是 if 的 then 形式,并且 func 必须出现在对 fboundp 的调用中。 (此功能也适用于 cond。) 同样,您可以通过在 boundp 测试上对其进行条件化来抑制对变量变量的特定使用的警告:

(if (boundp 'variable) ...variable...)

对变量的引用必须是 if 的 then 形式,并且变量必须出现在对 boundp 的调用中。 您可以告诉编译器一个函数是使用 declare-function 定义的。 请参阅告诉编译器定义了一个函数。 同样,您可以告诉编译器一个变量是使用 defvar 定义的,没有初始值。 (请注意,这会将变量标记为特殊的,即动态绑定,但仅在当前词法范围内,或者如果在顶层,则为文件。)请参阅定义全局变量。

您还可以使用 with-suppressed-warnings 宏在某个表达式中抑制编译器警告:

Special Form: with-suppressed-warnings warnings body… ¶

在执行中,这等价于 (progn body…),但编译器不会针对 body 中的指定条件发出警告。 warnings 是它们适用的警告符号和函数/变量符号的关联列表。 例如,如果您想调用一个名为 foo 的过时函数,但又想禁止编译警告,请说:

     (with-suppressed-warnings ((obsolete foo))
	(foo ...))

要更粗粒度地抑制编译器警告,您可以使用 with-no-warnings 构造:

Special Form: with-no-warnings body… ¶

在执行中,这等价于 (progn body…),但编译器不会对 body 内部发生的任何事情发出警告。

我们建议您改用 with-suppressed-warnings,但如果您确实使用此构造,请在可能的最小代码段周围使用它,以避免错过可能的警告,而不是您打算禁止的警告。

通过设置变量 byte-compile-warnings 可以更精确地控制字节编译器警告。 有关详细信息,请参阅其文档字符串。

有时您可能希望使用错误报告字节编译器警告。 如果是这样,请将 byte-compile-error-on-warn 设置为非零值。

17.7 字节码函数对象

字节编译函数有一种特殊的数据类型:它们是字节码函数对象。 每当这样的对象作为要调用的函数出现时,Emacs 就会使用字节码解释器来执行字节码。

在内部,字节码函数对象很像一个向量。 可以使用 aref 访问其元素。 它的打印表示类似于矢量,在开头的“[”之前有一个附加的“#”。 它必须至少有四个元素; 没有最大数量,但只有前六个元素可以正常使用。 他们是:

argdesc

参数的描述符。 这可以是参数列表,如参数列表的特性中所述,也可以是编码所需参数数量的整数。 在后一种情况下,描述符的值指定第 0 到 6 位中的最小参数数量,以及第 8 到 14 位中的最大参数数量。如果参数列表使用 &rest,则设置第 7 位; 否则它被清除。

如果 argdesc 是一个列表,则参数将在执行字节码之前动态绑定。 如果 argdesc 是整数,则在执行代码之前,参数将被推送到字节码解释器的堆栈中。

byte-code

包含字节码指令的字符串。

constants

字节码引用的 Lisp 对象的向量。 这些包括用作函数名和变量名的符号。

stacksize

此函数所需的最大堆栈大小。

docstring

文档字符串(如果有); 否则,无。 如果文档字符串存储在文件中,则该值可以是数字或列表。 使用函数文档获取真正的文档字符串(请参阅访问文档字符串)。

interactive

交互式规范(如果有)。 这可以是字符串或 Lisp 表达式。 对于非交互式功能,它是 nil。

这是一个字节码函数对象的示例,以印刷形式表示。 它是命令backward-sexp 的定义。

#[256
  "\211\204^G^@\300\262^A\301^A[!\207"
  [1 forward-sexp]
  3
  1793299
  "^p"]

创建字节码对象的原始方法是使用 make-byte-code:

Function: make-byte-code &rest elements ¶

该函数构造并返回一个以元素为元素的字节码函数对象。

您不应该尝试自己提出字节码函数的元素,因为如果它们不一致,Emacs 可能会在您调用该函数时崩溃。 始终将其留给字节编译器来创建这些对象; 它使元素保持一致(我们希望)。

17.8 反汇编字节码

人们不写字节码; 该工作留给字节编译器。 但是我们提供了一个反汇编程序来满足猫一样的好奇心。 反汇编器将字节编译的代码转换为人类可读的形式。

字节码解释器被实现为一个简单的堆栈机器。 它将值推送到自己的堆栈中,然后将它们弹出以在计算中使用它们,其结果本身被推回堆栈中。 当字节码函数返回时,它会从堆栈中弹出一个值并将其作为函数的值返回。

除了堆栈之外,字节码函数可以通过在变量和堆栈之间传输值来使用、绑定和设置普通的 Lisp 变量。

Command: disassemble object &optional buffer-or-name ¶

此命令显示对象的反汇编代码。 在交互式使用中,或者如果 buffer-or-name 为 nil 或省略,则输出进入名为 Disassemble 的缓冲区。 如果 buffer-or-name 不为 nil,则它必须是缓冲区或现有缓冲区的名称。 然后输出到那里,点,点在输出之前。

参数对象可以是函数名称、lambda 表达式(请参阅 Lambda 表达式)或字节码对象(请参阅字节码函数对象)。 如果它是一个 lambda 表达式,则 disassemble 对其进行编译并反汇编生成的编译代码。

这里有两个使用反汇编函数的例子。 我们添加了解释性注释来帮助您将字节码与 Lisp 源代码相关联; 这些不会出现在 disassemble 的输出中。

(defun factorial (integer)
  "Compute factorial of an integer."
  (if (= 1 integer) 1
    (* integer (factorial (1- integer)))))
     ⇒ factorial


(factorial 4)
     ⇒ 24


(disassemble 'factorial)
     -| byte-code for factorial:
 doc: Compute factorial of an integer.
 args: (integer)


0   varref   integer      ; Get the value of integer and
			    ;   push it onto the stack.
1   constant 1            ; Push 1 onto stack.

2   eqlsign               ; Pop top two values off stack, compare
			    ;   them, and push result onto stack.

3   goto-if-nil 1         ; Pop and test top of stack;
			    ;   if nil, go to 1, else continue.
6   constant 1            ; Push 1 onto top of stack.
7   return                ; Return the top element of the stack.

8:1 varref   integer      ; Push value of integer onto stack.
9   constant factorial    ; Push factorial onto stack.
10  varref   integer      ; Push value of integer onto stack.
11  sub1                  ; Pop integer, decrement value,
			    ;   push new value onto stack.
12  call     1            ; Call function factorial using first
			    ;   (i.e., top) stack element as argument;
			    ;   push returned value onto stack.

13 mult                   ; Pop top two values off stack, multiply
			    ;   them, and push result onto stack.
14 return                 ; Return the top element of the stack.

silly-loop 函数稍微复杂一些:

(defun silly-loop (n)
  "Return time before and after N iterations of a loop."
  (let ((t1 (current-time-string)))
    (while (> (setq n (1- n))
		0))
    (list t1 (current-time-string))))
     ⇒ silly-loop


(disassemble 'silly-loop)
     -| byte-code for silly-loop:
 doc: Return time before and after N iterations of a loop.
 args: (n)


0   constant current-time-string  ; Push current-time-string
				    ;   onto top of stack.

1   call     0            ; Call current-time-string with no
			    ;   argument, push result onto stack.

2   varbind  t1           ; Pop stack and bind t1 to popped value.

3:1 varref   n            ; Get value of n from the environment
			    ;   and push the value on the stack.
4   sub1                  ; Subtract 1 from top of stack.

5   dup                   ; Duplicate top of stack; i.e., copy the top
			    ;   of the stack and push copy onto stack.
6   varset   n            ; Pop the top of the stack,
			    ;   and bind n to the value.

;; (In effect, the sequence dup varset copies the top of the stack
;; into the value of n without popping it.)


7   constant 0            ; Push 0 onto stack.
8   gtr                   ; Pop top two values off stack,
			    ;   test if n is greater than 0
			    ;   and push result onto stack.

9   goto-if-not-nil 1     ; Goto 1 if n > 0
			    ;   (this continues the while loop)
			    ;   else continue.

12  varref   t1           ; Push value of t1 onto stack.
13  constant current-time-string  ; Push current-time-string
				    ;   onto the top of the stack.
14  call     0            ; Call current-time-string again.

15  unbind   1            ; Unbind t1 in local environment.
16  list2                 ; Pop top two elements off stack, create a
			    ;   list of them, and push it onto stack.
17  return                ; Return value of the top of stack.
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/advanceflow/elisp.git
git@gitee.com:advanceflow/elisp.git
advanceflow
elisp
Elisp
main

搜索帮助