[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端]

15. バイトコンパイル

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp21&node=Byte%20Compilation"
"texi/elisp21/バイトコンパイル"へのコメント(無し)

Emacs Lispには、Lispで書いた関数を より効率よく実行可能なバイトコード(byte-code)と呼ばれる 特別な表現に変換するコンパイラ(compiler)があります。 コンパイラはLispの関数定義をバイトコードで置き換えます。 バイトコード関数を呼び出すと、 バイトコードインタープリタ(byte-code interpreter)が その定義を評価します。

(真のコンパイル済みコードのように)計算機ハードウェアが直接実行するかわりに、 バイトコードインタープリタがバイトコンパイル済みのコードを評価するので、 バイトコードは、再コンパイルせずに計算機から計算機に移せます。 しかしながら、真のコンパイル済みコードほど速くはありません。

EmacsバイトコンパイラがLispファイルをコンパイルするときには、 `--unibyte'を指定してEmacsを起動したとしても、 ファイルで特に指定しなければ、 つねにファイルをマルチバイトテキストとして読みます。 コンパイルしても、コンパイルせずに同じファイルを実行した場合と同じ結果を 得るようにするためです。 See 節 14.3 非ASCII文字のロード

一般に、Emacsの任意の版は、それよりまえの版でバイトコンパイルしたコードを 実行できますが、その逆は真ではありません。 Emacs 19.29では互換性のない大きな変更を行いましたから、 それ以降の版でコンパイルしたファイルは、 特別なオプションを指定しない限り、それ以前の版ではまったく動きません。 さらに、Emacs 19.29では、キーボード文字の修飾ビットを変更しました。 その結果、19.29よりまえの版でコンパイルしたファイルは、 修飾ビットを含む文字定数を使っているとそれ以降の版では動作しません。

バイトコンパイル中に生起するエラーについては、 See 節 17.4 コンパイル時の問題のデバッグ。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端]

15.1 バイトコンパイルコードの性能

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp21&node=Speed%20of%20Byte-Code"
"texi/elisp21/バイトコンパイルコードの性能"へのコメント(無し)

バイトコンパイルした関数は、Cで書いた基本関数ほど効率よくはありませんが、 Lispで書いた版よりはよほど速く動きます。 例を示しましょう。

 
(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

(silly-loop 100000)
=> ("Fri Mar 18 17:25:57 1994"
    "Fri Mar 18 17:26:28 1994")  ; 31秒

(byte-compile 'silly-loop)
=> [コンパイルしたコードは省略]

(silly-loop 100000)
=> ("Fri Mar 18 17:26:52 1994"
    "Fri Mar 18 17:26:58 1994")  ; 6秒

この例では、解釈実行するコードでは実行に31秒必要でしたが、 バイトコンパイルしたコードでは6秒でした。 この結果は代表的なのもですが、実際の結果は大きく変動します。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端]

15.2 コンパイル関数

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp21&node=Compilation%20Functions"
"texi/elisp21/コンパイル関数"へのコメント(無し)

関数byte-compileで、 個々の関数定義やマクロ定義をバイトコンパイルできます。 byte-compile-fileで1つのファイル全体をコンパイルしたり、 byte-recompile-directorybatch-byte-compileで 複数個のファイルをコンパイルできます。

バイトコンパイラは、 各ファイルに対するエラーメッセージや警告メッセージを `*Compile-Log*'と呼ばれるバッファに出力します。 読者のプログラムに関してここに報告されたことがらは、 問題点を指摘しますが、必ずしもエラーとは限りません。

バイトコンパイルする可能性のあるファイルにマクロ呼び出しを書くときには 注意してください。 マクロ呼び出しはコンパイル時に展開されるので、 正しくコンパイルするためにはマクロは定義済みである必要があります。 詳しくは、See 節 12.3 マクロとバイトコンパイル

通常、ファイルをコンパイルしてもファイルの内容を評価したり、 ファイルをロードしません。 しかし、ファイルのトップレベルに書いたrequireは実行します。 コンパイル時に必要なマクロ定義が存在することを保証する1つの方法は、 それらを定義するファイルを要求(require)することです (see 節 14.6 機能)。 コンパイルしたプログラムを実行するときに マクロ定義ファイルのロードを防ぐには、 requireの呼び出しの周りにeval-when-compileを書きます (see 節 15.5 コンパイル時の評価)。

Function: byte-compile symbol
この関数は、symbolの関数定義をバイトコンパイルし、 以前の定義をコンパイルしたもので置き換える。 symbolの関数定義は、関数の実際のコードであること。 つまり、コンパイラは、別のシンボルへの間接参照を辿らない。 byte-compileは、symbolのコンパイル済みの新たな定義を返す。

symbolの定義がバイトコード関数オブジェクトであると、 byte-compileはなにもせずにnilを返す。 Lispはどんなシンボルに対しても関数定義を1つだけ記録するので、 それがすでにコンパイル済みであると、 コンパイルまえのコードはどこにもないのである。 したがって、『同じ定義をコンパイルし直す』方法はない。

 
(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."]

結果は、バイトコード関数オブジェクトである。 この文字列には実際のバイトコードが入っている。 その各文字は、命令や命令のオペランドである。 ベクトルには、特別な命令に符号化される特定の基本関数を除いて、 関数が使うすべての定数、変数名、関数名が入っている。

コマンド: compile-defun
このコマンドはポイントを含むdefunを読み取り、 それをコンパイルして、結果を評価する。 実際に関数定義であるdefunでこのコマンドを使うと、 その関数をコンパイルしたものをインストールすることになる。

コマンド: byte-compile-file filename
この関数は、filenameという名前のLispコードのファイルを コンパイルしバイトコードのファイルにする。 出力ファイルの名前は、接頭辞`.el'を`.elc'に換えて作る。 filenameが`.el'で終っていないときには、 filenameの末尾に`.elc'を付加する。

入力ファイルから一度に1つずつフォームを読みながらコンパイルを行う。 それが関数定義やマクロ定義であると、 コンパイルした関数定義やマクロ定義を書き出す。 他のフォームは一塊にして、各塊をコンパイルして書き出し、 ファイルを読むとコンパイルしたコードが実行されるようにする。 入力ファイルを読むときにすべてのコメントを捨てる。

このコマンドはtを返す。 対話的に呼び出すとファイル名を問い合わせる。

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

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

% ls -l push*
-rw-r--r--  1 lewis     791 Oct  5 20:31 push.el
-rw-rw-rw-  1 lewis     638 Oct  8 20:25 push.elc

コマンド: byte-recompile-directory directory flag
この関数は、directoryにある再コンパイルが必要な 個々の`.el'ファイルを再コンパイルする。 ファイルを再コンパイルする必要があるのは、 `.elc'ファイルが存在しても`.el'ファイルより古い場合である。

`.el'ファイルに対応する`.elc'ファイルが存在しない場合には、 flagが動作を指示する。 それがnilであると、そのようなファイルは無視する。 nil以外であると、そのような各ファイルをコンパイルするかどうか ユーザーに問い合わせる。

このコマンドの戻り値は予測できない。

Function: batch-byte-compile
この関数は、コマンド行に指定したファイル群に対して byte-compile-fileを実行する。 この関数はEmacsをバッチモードで実行しているときにだけ使うこと。 完了するとEmacsを終了するからである。 1つのファイルでエラーが発生しても、後続のファイルの処理には影響しないが、 エラーを起こしたファイルに対する出力ファイルは生成せず、 Emacsのプロセスは0以外の状態コードで終了する。

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

Function: byte-code code-string data-vector max-stack
この関数はバイトコードを実際に解釈実行する。 バイトコンパイルした関数は、実際には、 byte-codeを呼び出すような本体として定義される。 この関数を読者自身で呼び出さないこと。 この関数の正しい呼び出しを生成する方法はバイトコンパイラだけが知っている。

Emacs 18版では、バイトコードは関数byte-codeをつねに呼び出すことで 実行していた。 現在では、バイトコード関数オブジェクトの一部としてバイトコードを実行するのが 普通であり、byte-codeを明示的に呼び出すことは稀である。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端]

15.3 説明文字列とコンパイル

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp21&node=Docs%20and%20Compilation"
"texi/elisp21/説明文字列とコンパイル"へのコメント(無し)

バイトコンパイルしたファイルからロードした関数や変数では、 それらの説明文字列は、必要に応じてそのファイルを動的に参照します。 これはEmacs内のメモリを節約しロード処理も速くなります。 というのは、ファイルのロード処理で説明文字列を処理する必要がないからです。 説明文字列を実際に参照するのは遅くなりますが、 普通、ユーザーをいらいらさせるほとではありません。

説明文字列を動的に参照することには欠点があります。

読者のサイトでEmacsを通常の手順でインストールした場合には、 これらの問題は普通起こらないはずです。 新版のインストールには別のディレクトリを使いますから、 旧版をインストールしてある限り、そのファイル群は意図した場所に 無変更で残っているはずです。

しかしながら、読者自身がEmacsを構築して、 構築したディレクトリからEmacsを使う場合、 Lispファイルを編集して再コンパイルすると、 しばしばこの問題を経験するでしょう。 そのような場合には、再コンパイルしたあとでファイルを再ロードすれば 問題を解決できます。

旧版ではこの機能を使えないので、 Emacsの(19.29以降の)最近の版でバイトコンパイルしたファイルは 旧版ではロードできません。 byte-compile-dynamic-docstringsnilを設定すれば、 コンパイル時にこの機能をオフにできます。 Emacsの旧版にロードできるようにファイルをコンパイルできるのです。 すべてのファイルをこのようにコンパイルしたり、あるいは、 この変数をファイルにローカルな束縛に指定して1つのソースファイルだけを このようにコンパイルしたりもできます。 そのようにする1つの方法は、つぎの文字列をファイルの先頭行に追加することです。

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

Variable: byte-compile-dynamic-docstrings
これがnil以外であると、 バイトコンパイラは、説明文字列を動的にロードするように設定した コンパイル済みファイルを生成する。

説明文字列を動的に扱う場合、 コンパイル済みのファイルではLispリーダの特別な構文`#@count'を 使います。 この構文は後続のcount文字を読み飛ばします。 また、`#$'という構文も使います。 これは、『文字列としてのこのファイルの名前』を表します。 Lispのソースファイルでは、これらの構文を使わないのが最良です。 これらは人が読むファイル向けに設計したものではないからです。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端]

15.4 個別関数の動的ロード

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp21&node=Dynamic%20Loading"
"texi/elisp21/個別関数の動的ロード"へのコメント(無し)

ファイルをコンパイルするとき、 動的関数ロード(dynamic function loading、 遅延ロード(lazy loading)ともいう)機能を指定できます。 動的関数ロードでは、ロードするときにファイル内の関数定義をすべて 読むわけではありません。 そのかわりに、各関数定義には、 そのファイルを指す埋め草が入っています。 それぞれの関数を初めて呼び出したときに、 その完全な定義をファイルから読み取り、埋め草を置き換えます。

動的関数ロードの利点は、ファイルをロードするよりかなり速いことです。 ユーザーが呼び出せる数多くの別々の関数を収めたファイルにおいては、 それらの1つだけを使って残りのものを使わないのであれば、 これは有利なことです。 キーボードコマンドを提供する特別なモードには、 しばしばこのような使い方のパターンがあります。 ユーザーがモードを起動しても、提供するコマンドの一部しか使わないのです。

動的関数ロードの機能には、ある種の欠点もあります。

Emacsのファイル群をインストールした普通の状況では、 このような問題は起きないはずです。 しかし、Lispファイルを読者が変更すると起こりえます。 これらの問題を回避するもっとも簡単な方法は、 再コンパイルするたびに新たにコンパイルしたファイルを ただちに再ロードすることです。

バイトコンパイラは、コンパイル時に変数byte-compile-dynamicnil以外であれば、動的関数ロードの機能を使います。 動的ロードは特定のファイルで必要なだけですから、 この変数をグローバルに設定しないでください。 そのかわりにファイルにローカルな変数束縛を使って 特定のソースファイルだけでこの機能をオンにします。 たとえば、ソースファイルの先頭行につぎのテキストを書けば、 そのようにできます。

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

Variable: byte-compile-dynamic
これがnil以外であると、 バイトコンパイラは、動的関数ロードを使うように設定した コンパイル済みのファイルを生成する。

Function: fetch-bytecode function
functionを完全にロードしていないと、 バイトコンパイルしたファイルからただちにfunctionの定義をロードする。 引数functionは、バイトコード関数オブジェクトか関数名である。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端]

15.5 コンパイル時の評価

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp21&node=Eval%20During%20Compile"
"texi/elisp21/コンパイル時の評価"へのコメント(無し)

プログラムのコンパイル時に評価されるようなコードを書くための機能です。

Special Form: eval-and-compile body
このフォームは、コンパイルしたり実行したり (コンパイルしてあってもしてなくても)するときに bodyを評価するように印を付ける。

bodyを別のファイルに収め、そのファイルをrequireで参照しても 同じ結果を得ることができる。 bodyが大きい場合には、そのほうが好ましい。

Special Form: eval-when-compile body
このフォームは、コンパイルしたプログラムをロードするときではなく、 プログラムのコンパイル時にbodyを評価するように印を付ける。 コンパイラが評価した結果は、コンパイルしたプログラム内に定数として現れる。 ソースファイルをコンパイルせずにロードすると、 bodyを普通どおり評価する。

Common Lispに関した注意: トップレベルでは、 Common Lispの(eval-when (compile eval) ...)の常套句に似ている。 それ以外の箇所では、Common Lispの`#.'リーダマクロは (解釈実行時ではなければ)eval-when-compileが行うことに近い。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端]

15.6 バイトコード関数オブジェクト

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp21&node=Byte-Code%20Objects"
"texi/elisp21/バイトコード関数オブジェクト"へのコメント(無し)

バイトコンパイルした関数は、特別なデータ型、 バイトコード関数オブジェクト(byte-code function objects)です。

内部的には、バイトコード関数オブジェクトはベクトルによく似ています。 しかし、評価時にこのデータ型が呼び出すべき関数として現れると、 特別に扱います。 バイトコード関数オブジェクトの表示表現はベクトルに似ていますが、 開き角括弧`['のまえに余分に`#'が付きます。

バイトコード関数オブジェクトには、少なくとも4つの要素が必要です。 最大個数に制限はありませんが、最初の6つ個の要素にだけ 普通の用途があります。 つぎのとおりです。

引数リスト
引数シンボルのリスト。

バイトコード
バイトコード命令を収めた文字列。

定数群
バイトコードが参照するLispオブジェクトのベクトル。 関数名や変数名として使われるシンボルを含む。

スタックサイズ
この関数に必要なスタックサイズの最大値。

説明文字列
(あれば)説明文字列。 さもなければnil。 説明文字列がファイルに収めてあれば、値は数かリストである。 実際の説明文字列を取得するには関数documentationを使う (see 節 23.2 説明文字列の参照)。

対話指定
(あれば)対話指定。 これは文字列かLisp式。 対話的でない関数ではnil

バイトコード関数オブジェクトの例を表示表現でつぎに示します。

 
#[(&optional arg)
  "^H\204^F^@\301^P\302^H[!\207"
  [arg 1 forward-sexp]
  2
  254435
  "p"]

バイトコードオブジェクトを作る基本的な方法は、 make-byte-codeを使うことです。

Function: make-byte-code &rest elements
この関数は、elementsを要素とする バイトコード関数オブジェクトを作成し返す。

バイトコード関数の要素を自分で作ったりしないでください。 それらに整合性がないと、 その関数を呼び出すとEmacsがクラッシュすることもあります。 これらのオブジェクトの作成は、バイトコンパイラに任せるべきです。 バイトコンパイラは整合した要素を作成します(と期待する)。

バイトコードオブジェクトの要素はarefで参照できます。 同じ要素群のベクトルをvconcatで作ることもできます。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端]

15.7 バイトコードの逆アセンブル

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=elisp21&node=Disassembly"
"texi/elisp21/バイトコードの逆アセンブル"へのコメント(無し)

人間はバイトコードを書きません。 それはバイトコンパイラの仕事です。 しかし、好奇心を満たすために逆アセンブラを用意してあります。 逆アセンブラはバイトコンパイルしたコードを人が読める形式に変換します。

バイトコードインタープリタは、単純なスタックマシンとして実装してあります。 値を自前のスタックに積み、計算に使うためにスタックから取り出し、 計算結果そのものはスタックにまた積みます。 バイトコード関数から戻るときには、スタックから値を取り出して 関数値としてその値を返します。

スタックに加えて、変数とスタックのあいだで値を転送することで、 バイトコード関数は、普通のLisp変数を使ったり、 束縛したり、値を設定できます。

コマンド: disassemble object &optional stream
この関数はobjectの逆アセンブルしたコードを出力する。 streamを指定すると、そこへ出力する。 さもなければ、逆アセンブルしたコードはストリームstandard-outputへ 出力する。 引数objectは関数名かラムダ式である。

特別な例外として、この関数を対話的に使うと、 `*Disassemble*'という名前のバッファへ出力する。

disassemble関数の使用例を2つ示します。 バイトコードと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   constant 1              ; スタックに1を積む

1   varref   integer        ; 環境からintegerの値を取得し、 
                            ; スタックに積む

2   eqlsign                 ; スタックの先頭から2つの値を
                            ; 取りさって比較し、
                            ; 結果をスタックに積む

3   goto-if-nil 10          ; スタックの先頭から値を取りさり
                            ; 検査する。nilならば10へ飛び、
                            ; さもなければつぎへ進む

6   constant 1              ; スタックに1を積む

7   goto     17             ; 17へ飛ぶ(この場合、関数は1を返す)

10  constant *              ; スタックにシンボル*を積む

11  varref   integer        ; スタックにintegerの値を積む

12  constant factorial      ; スタックにfactorialを積む

13  varref   integer        ; スタックにintegerの値を積む

14  sub1                    ; スタックからintegerを取りさり、
                            ; 減した新たな値をスタックに積む

                            ; スタックの現在の内容はつぎのとおり
                            ; - integerを減らした値
                            ; - factorial 
                            ; - integerの値
                            ; - *

15  call     1              ; スタックの最初(先頭)要素を使って
                            ; 関数factorialを呼び出す
                            ; 戻り値をスタックに積む

                            ; スタックの現在の内容はつぎのとおり
                            ; - factorialの
                            ;      再帰呼び出しの結果
                            ; - integerの値
                            ; - *

16  call     2              ; スタックの最初の要素の2つ
                            ; (先頭の2つ)を引数として
                            ; 関数*を呼び出し
                            ; 結果をスタックに積む

17  return                  ; スタックの先頭要素を返す
     => nil

関数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  ; current-time-stringを
                                  ; スタックの先頭に積む

1   call     0              ; 引数なしでcurrent-time-stringを
                            ; 呼び出し、結果をスタックに積む

2   varbind  t1             ; スタックから値を取りさり、
                            ; t1に束縛する

3   varref   n              ; 環境からnの値を取得し、
                            ; 値をスタックに積む

4   sub1                    ; スタックの先頭から1を引く

5   dup                     ; スタックの先頭の値を複製する
                            ; つまり、スタックの先頭の値を
                            ; コピーして、それをスタックに積む

6   varset   n              ; スタックの先頭から値を取りさり、
                            ; 値をnに束縛する

                            ; つまり、dup varsetは
                            ; スタックの先頭の値を取りさらずに
                            ; nにコピーする

7   constant 0              ; スタックに0を積む

8   gtr                     ; スタックから2つの値を取りさり、
                            ; nが0より大きいか調べ、
                            ; 結果をスタックに積む

9   goto-if-nil-else-pop 17 ; n <= 0ならば17へ飛ぶ
                            ; (whileループから抜ける)
                            ; さもなければ、スタックの先頭から
                            ; 値を取りさり、つぎへ進む

12  constant nil            ; スタックにnilを積む
                            ; (これはループの本体)

13  discard                 ; ループの本体の結果を捨てる
                            ; (whileループは副作用のために
                            ; つねに評価される)

14  goto     3              ; whileループの先頭へ飛ぶ

17  discard                 ; スタックの先頭の値を取りさって、
                            ; whileループの結果を捨てる。
                            ; これは、9での飛び越しのために
                            ; 取りさっていない値nil

18  varref   t1             ; t1の値をスタックに積む

19  constant current-time-string  ; current-time-stringを 
                                  ; スタックに積む

20  call     0              ; ふたたびcurrent-time-stringを
                            ; 呼び出す

21  list2                   ; スタックの先頭から2つの値を取りさり
                            ; それらのリストを作り、
                            ; リストをスタックに積む

22  unbind   1              ; ローカルの環境のt1の束縛を解く

23  return                  ; スタックの先頭の値を返す

     => nil


[ << ] [ >> ]           [表紙] [目次] [索引] [検索] [上端 / 下端]