GNU Common Lispを使う

たけおか@AXE (ex.山本ナヲミ)(竹岡尚三)

#| これは、筆者が
技術評論社 刊 ソフトウェアデザイン誌
「つこてなんぼのFreeBSD」
1998年9月
に掲載した原稿をもとにしたものです |#


0. はじめに

筆者がSun3(CPU=68020)のSunOS下で、CommonLispを初めて触った時、あまりに 快適なので驚きました。
当時、ワークステーションと呼ばれる機械の性能が急激に上がり始めた頃です。
CommonLispの様に規模の大きな言語は仮想記憶がなければまともに動かすこと はできません。当然、それまでは、かなり大きなスーパーミニコン(VAX-11や MV-2000などという機械)以上の計算機でしか動作しませんでした。
当時もDOS用に、GoldenCommonLispなどいくつかの「CommonLisp準拠」と自称す る処理系がありましたが、どれも機能が小さすぎ、まったくCommonLispとは呼べ ないようなしろものばかりでした。
また、当時のUNIXをとりまく環境は X Window SystemもVer.10が出る前で、ウ インドウシステムと言えば、各社個別であるのが当たりまえで、SUNではSunView を使用しました。また、UNIXのバージョンは SystemVはR1が出るか出ないかとい う頃で、当然、System IIIやSystemV では仮想記憶が動かないのでした。
当時、ワークステーション用OSも各社個別に制作し競争をしていたのですが、 結局のところ、最後に残ったのはUNIXですね。
当時、UNIXであり、かつ仮想記憶がバンバン使えるのはBSDしかない、というこ とで、筆者はBSDの信者になってしまったのです。
今回は、筆者の回りで使われている、(かつては)人工知能向けと言われた言語 CommonLispを紹介します。



1. KClについて

上記の時に、筆者が使用していたCommonLispはKyotoCommonLisp(略称KCl)とい うものです。
KClは当時京都大学におられた湯浅太一先生(豊橋技科大を経て現京大)萩谷昌己 先生(現東大)のお二人が、 CommonLispの仕様書を読んだだけで作られたもので す。
CommonLispはそれまでのアメリカLisp界の2大潮流であるZetaLispとInterLisp を統合し、ソフトウェア産業界で使用できる共通のLispを定めようとしてできた ものです。
ZetaLispもInterLispもその仕様の大きさは有名だったのですが、それらを統合 するにあたり、CommomLispも非常に大きな仕様を持つことになりました。
で、その巨大なLispを東洋の片隅で2人の人間が作ったので、アメリカ人も大変 に驚いたということです。
今回、紹介するGNU Common Lisp(GCL)は実はこのKClがGNUに寄付されて、名前 がGCLに変っただけのものです。
GNUのStallmanと言えば、Lispの本家であるMIT AI Lab. の出身で、Lisp処理 系をバリバリと作ってしまうおじさんなのですが、そのGNUがKClをもらって、 GCLにしたということは、 KClの凄さを何よりも現していると言えるでしょう。

KCl(GCL)は色々とすぐれているのですが、その中でも、もっとも特徴的ですぐ れている点は、そのほとんどがC言語で書かれている。それに加えて、Lispコン パイラの生成するオブジェクト・コードがC言語のソースとなっている点です。
CommonLispを作り、移植性を持たせるのは、大変なことです。しかも、性能を 良くするのは非常に大変なことです。
移植性があり、性能のよいコンパイラを作るのも非常に難しいことです。KClは オブジェクト・コードを C言語とすることで、いとも簡単にその壁を乗り越えて います。
実は、当時は筆者は「Cのソースを出すなんて単なる手抜きか?」と思っていた のですが…
その後、SparcやMipsを始めとするRISCの時代になり、C言語コンパイラの最適 化が非常に良くなり、へたなコンパイラではどうしようもない時代になります。
また、CPUの世代交代のサイクルが早くなり、とてもLispコンパイラで機械語を 扱っていられない時代になってきました。
この点、KClのコンパイラは機械に依存する最適化をCコンパイラに任せている ため、非常によい最適化が行われます。
逆に、例えば、エール大学のT言語(Lispの一種)処理系は機械語を直接に出力す るコンパイラを持っていますが、それのSparc版などは、いつもほとんど決りきっ たコードをつなぎ合わせて吐き出すだけで、レジスタの詰合せなどはほとんどやっ てくれません。また、このT言語処理系は開発が止まっているので、SuperSparc などのスーパースカラCPUなどには、対応のしようもなく、どうにもなりません。
KClであれば、Sparcなどは、SUNのコンパイラが賢くなったら、それだけで、最 適化が進歩します。
いまでは、CPUのモデルごとに異るキャッシュの量やALUの数などに応じて、個 別に最適化を行わなければならないので、もうC言語コンパイラに頼らなければ、 移植性があり高性能なコンパイラを作ることは難しいでしょう。

この素晴らしいKClがGCLとなって、いますぐFreeBSDで使えます。
ただし、現在、 CommonLispはその規格が第2版に更新され、オブジェクト指向 サポート機構などが仕様に含まれていますが、KCLはCommonLisp第1版準拠なので、 仕様が古くなっています。
KClからGCLとなって、手は加えられているようですが、第2版仕様にまでしては くれていないようです。
GCLはCommonLisp第1版とはいえ、計算機のパワーが非常に大きい今こそ、仕事 に便利に使えます。
例えば、CommonLispは32bitを越える大きな整数をなんの苦もなく扱えるので、 筆者の回りでは、280 bitも幅のある水平型マイクロコードのアセンブラを CommonLispで書いたりしています。



3. GCLをFreeBSDで使う

GCLはFreeBSDのパッケージにすでに入っているので、すぐにインストールして 使用できます。
ただし、CommonLispを本気で使用する場合はスワップ領域を充分に取っておい て方がよいでしょう。X WindowとMule(GNU Emacs)も同時に使用するなら、今な ら、 150Mバイト締度のスワップ領域は用意した方がよいと思われます。(筆者は スワップが40MバイトしかないノートPCでもこれらを使用しています。しかし、 そのマシンでは大きな仕事はしていません)

インストールには、packages/lang/gcl*を使用します。
筆者はFreeBSD2.2.2を使用しているので、gcl-2.0を使用しました。


 # pkg_add gcl-2.0.tgz
 
とします。
結果として、
/usr/local/binにgclが
/usr/local/lib/gcl/に様々なファイルが
置かれます。

早速起動してみましょう。


% rehash
% gcl
GCL (GNU Common Lisp)  Version(2.0) Thu Mar  7 06:44:49 PST 1996
Licensed under GNU Public Library License
Contains Enhancements by W. Schelter

>(car '(a s d))
A

>
 
いい感じですね。

16進数電卓としても使えます。


>(+ #x11 9)
26
 
「#x」は16進数を表す接頭語です。ここでは、0x11(17) + 9を計算して、答が 26と出ています。
16進数表示も簡単にできます。

>(format t "~x" (+ #x11 9))
1A
NIL
 

formatはC言語のfprintfのようなフォームです。第1引数のtは標準出力を指定 し、 "~x"は印字フォーマット指定で、16進数表示を指定します。
この整数演算は非常に大きな数が扱えます。こういう巨大な数字をビッグナム (Big Num)といい、大抵のLisp系の言語はBigNumを扱う能力を持っています。
BigNumがあれば、計算中にオーバーフローしておかしな値になることを気にせ ずに済みます。また、問題があるシステムのどこかで数値がオーバーフローして 変な動作をしていないかの検算にLispを使用すると、非常に便利です。
例えば、

>(format t "~x" (+ #x123456789abcdef00000 1))
123456789ABCDEF00001
NIL
 
で80bitの加算ができています。

GCLは'('に対応するだけの')'が打ち込まれないと、打ち込まれた式の評価(実 行)を始めません。従って、リターン・キーを押下しても、ただ画面上で改行が 起こるだけです。
GCLが何も反応しなくて、おかしいなと思ったら、とりあえず ')'をたくさん入 力して下さい。多すぎる')'は無視されるので問題ありません。

また、実行時エラーが起きると、次のようなブレーク・レベルが起動されます。
(この例では、未定義な関数「asd」を実行しようとして、エラーになった)


>(asd)

Error: The function ASD is undefined.
Fast links are on: do (use-fast-links nil) for debugging
Error signalled by EVAL.
Broken at EVAL.  Type :H for Help.
>>:q

Top level.
>
 

ブレーク・レベルではスタック・トレースを見たり、ローカル変数を参照、変 更したりと、色々なことができるのですが、とりあえずは、「:q」を入力すると、 通常のトップ・レベルへと戻れます。

作成したプログラムが無限ループなどに入ってしまった場合は、C-c(CTRL-C)を 入力するとブレークして、ブレーク・レベルに入ります。C-cを入力しても、ブ レークされるまで少しの間があるのですが、それは正常です。

プログラム・ファイルのロード(ここでは、tst.lspというファイルがカレント・ ディレクトリにあるとします)は、


>(load "tst.lsp")
Loading tst.lsp
Finished loading tst.lsp
T
 
とします。

プログラム・ファイルのコンパイルは、


>(compile-file "tst.lsp")
Compiling tst.lsp.
End of Pass 1.
End of Pass 2.
OPTIMIZE levels: Safety=0 (No runtime error checking), Space=0, Speed=3
Finished compiling tst.lsp.
#"tst.o"
 
です。
これで、tst.oができます。このtst.oは起動中のGCLにロードできます。

>(load "tst.o")
Loading tst.o
start address -T 22cb38 Finished loading tst.o
96
 
この様に起動中のプログラムにあとからダイナミックにコードを読み込みリン クすることを「インクリメンタル・ロード(またはリンク)」と呼びます。インタ プリタ言語にはよく実装されています。

GCLは関数単位のコンパイルも行えます。
まず、fa1(階乗関数)という関数を定義してみましょう。


>(defun fa1 (n i)
  (if (zerop n) i
    (fa1 (1- n) (* n i))))
FA1
 
このfa1をコンパイルするには

>(compile 'fa1)
Compiling gazonk0.lsp.
End of Pass 1.
;; Note: Tail-recursive call of FA1 was replaced by iteration.
End of Pass 2.
OPTIMIZE levels: Safety=0 (No runtime error checking), Space=0, Speed=3
Finished compiling gazonk0.lsp.
Loading gazonk0.o
start address -T 22df4c Finished loading gazonk0.o
#<compiled-function FA1
>
 
とします。

CommonLispにはディスアセンブル機能があります。本来は機械語になったルーチ ンをディスアセンブルして表示するものです。が、GCL(KCl)ではLispソースをC 言語へコンパイルしたものを表示します。:-) ディスアセンブル可能な関数はコ ンパイル前のものです。
従って、もう一度、fa1をdefunし直した後、disassembleを行うと、以下のよう な出力が得られます。



>(disassemble 'fa1)
Compiling gazonk0.lsp.End of Pass 1.  
#include <cmpinclude.h>
#include "gazonk0.h"
init_gazonk0(){do_init(VV);}
/*	function definition for FA1	*/

static L1()
{register object *base=vs_base;
	register object *sup=base+VM1; VC1
	vs_check;
	{object V1;
	object V2;
	V1=(base[0]);
	V2=(base[1]);
	vs_top=sup;
TTL:;
	if(!(number_compare(small_fixnum(0),(V1))==0)){
	goto T2;}
	base[2]= (V2);
	vs_top=(vs_base=base+2)+1;
	return;
T2:;
	{object V3;
	V3= one_minus((V1));
	V2= number_times((V1),(V2));
	V1= (V3);}
	goto TTL;
;; Note: Tail-recursive call of FA1 was replaced by iteration.
	}
}
End of Pass 2.  
OPTIMIZE levels: Safety=0 (No runtime error checking), Space=0, Speed=3
Finished compiling gazonk0.lsp.       
#(
#((system::%init . #((system::mf (lisp::quote user::fa1) 0) (system::debug (lisp::quote user::fa1) (lisp::quote (user::n user::i))))))
)

static L1();
#define VC1
#define VM1 3
static char * VVi[1]={
#define Cdata VV[0]
(char *)(L1)
};
#define VV ((object *)VVi)
T
 
この結果で、注目したいのは、fa1の中からfa1を再帰呼び出ししている部分が、 テイル・リカージョンのオプティマイズにより、goto TTL;に置き換えられてい ることでしょう。



4. GCLをEmacsと組み合わせて使う

shellからGCLを起動して使うとわかる通り、こういう対話型の言語を、ttyで使 用するのは非常に辛いものがあります。
なぜなら、たった1文字の打ち間違いで折角の入力がムダになってしまうからで す。
それをなんとかするには、Emacsの中からGCLを使用するのです。
すごいことに、GNU-Emacsには、Lispを起動するコマンドがあります。
Lisp起動コマンドでgclを起動するには、.emacs中などで
(setq inferior-lisp-program "gcl")
としてLisp処理系のコマンド名を設定しておきます。
では、muleを起動して、その中からgclを起動しましょう。

 % mule
 
muleが起動したら、M-xを打鍵し、ミニバッファで「run-lisp」と打ち込み、リ ターンを入力します。
すると、muleのバッファ内でGCLが起動します。
このバッファの中では、GCLと対話でき、しかもEmacsの編集コマンドが使用で きます。これで、打ち間違いを起こしても、大丈夫ですね。
次にソースを編集するために、C-x 2としてスクリーンを2つに分割し、片方で tst.lspを編集するバッファを開きます。



さて、Emacs+GCLのすごいところはこれからです。
ファイル編集バッファで色々と打ち込みます。tst.lspを編集中のファイル編集 バッファはLispモードになっているはずです。
関数一つだけを編集して、それをすぐにGCLに読み込ませたい時は、編集バッファ の関数定義のS式のすぐ後ろにカーソルを持っていき、C-x C-eと連打します。す るとS式が GCLに送られて、評価されます。
C-x C-eはカーソルの直前にあるS式を評価するので、関数定義のみでなく、ファ イル編集バッファ中の任意のS式を評価することができます。
関数定義をコンパイルしてロードしたい場合は、関数定義の式のすぐ後ろにカー ソルを置き、そこでC-c C-cと連打します。すると、関数定義がGCLに送られ、関 数をコンパイルした後、ロードします。
作成したプログラムが無限ループなどに入った場合は、GCLの動作しているウイ ンドウ(*inferior-lisp*)でC-c C-cと連打してください。これでブレークします。


もう、これだけあれば、Lispマシンは必要ありませんね。
AXEには、Symbolics(有名なLispマシン)があって、筆者もしばらく使いました が、 SysmbolicsLisp+Zmacsよりも、貧乏ながらもGCL+Muleの方が、今では手軽 かつ高速に動くので、最近ではSymbolicsにはほとんど火を入れていません。

--- EOF


たけおかの Lispページ 目次

Gnu Common Lisp(GCL)/Kyoto Common Lisp(KCL)の最適化について

CommonLispで記述した、StarTrek(1976年頃流行した古いゲーム)

Prologの入門文書に飽きた人に

たけおか(竹岡尚三)のホームページ