Sec-1.前書き(scheme)
Sec-2.比較する処理系
Sec-3.比較結果
Sec-4.速度の比較(簡易比較)
Sec-10.前書き(Lisp)
Sec-11.速度の比較(簡易比較)
Sec-12.文字列処理の実装
Sec-13.組み込利用について(ECL)
Sec-14.Lisp 各処理系の末尾再帰問題
Sec-15.scheme と Lisp のどちらを選ぶか
2014-10 に初稿を書きました。その時はファイル操作の関数などを比較しました。ただし結論としては「そういう比較をしても処理系の優劣は判断出来ない」というものでした、汗。
2025-03: 今回は、少し趣きを変えて全般網羅的にコメント・感想を羅列しました。タイトルは「比較」ですが、「単なる私の感想」です。
比較する処理系(望ましい処理系)は
としました。
組み込みについては chap-15. で触れています。昨今の自分の気持ちとしては「組み込み」だけでなく「組み込みライクなやり方」でも良いかなあ・・・なんて思っています。が、ここでは取りあえず「組み込み」だけに限定しました。
人気や評判については、人気投票ウェブサイトを参考にしつつ選びました。マイナー言語の scheme で人気投票を開催する人がいる事に驚きました。世の中は広いです、笑。
人気投票サイトは下記のとおり。
タイトル 81 Best scheme implementations as of 2024 - Slant URL https://www.slant.co/topics/5282/~scheme-implementations
結果的に、選んだのは chicken、guile、chibi-scheme でした。選んだ理由等を下記で説明します。
訂正:このチャプターを書きなおした後で判明しましたが、chibi は一部(string-contains)で古い仕様を捨てているようです (srfi-13 でなく srfi-130 を採用)。25とか30年くらい前は srfi-13 が標準だったと思います。なので、その部分では古いプログラムが動きません。イマイチです。
古いプログラムを走らせる場合、chibi だと string-contains は自作する必要があります、笑。そんなに難しく無いですが・・・
参考になる URL を以下に示します。
タイトル 人気の言語を作るには URL http://practical-scheme.net/trans/being-popular-j.html
Gauche の作者さんが翻訳したものが同サイトに掲載されています。とても面白いし参考になります。以下で、ちょっとだけ引用します。
このような素晴らしい文書を翻訳なさった Gauche 作者さんに感謝です。
第一に、大事な点・・安定性とか堅牢性とか
正確に動くとか、バグが少ないとか、安定しているとか、一番重要な部分です。
まず、人気と継続性、サポート規模等:
guile、chicken は処理系として長い歴史があります。サポートしているところも、大きなところです。なのでノー天気な発想ですが、人気から判断すれば大丈夫です。
chibi についてはサポート団体の正体がよく分かりません。大きいのか小さいのかも分かりません。ただし 2009 年頃には既に R7準拠等の関係で chibi を勧めている話がネット上にあります。つまり最低でも15年以上の実績があります。またコントリビュータの数が70人近くいるようです。そんなことなので取りあえず chibi もオケと考えました。人気の面やサポート等から見れば三者ともオケです。
使ってみての安定性
ただし chicken のコンパイラがやや微妙です。cond 文の中で
((symbol? data) (set! data (symbol->string data)))
とやったのですが、chicken のコンパイラーがこの部分でワーニングを出してきます。「symbol->string は symbol に対して適用するはずなのに、data に適用していて、これはまずいぞ」という警告です。そもそも symbol かどうか判定した上で symbol->string しているのだから、何も間違っていない。ここで data はただの変数です。他の処理系でここで警告やエラーを出すものはありません。どうも chicken のコンパイラーの一部に「バグ一歩手前」があるようです。(chicken のインタープリタはこの部分を問題なく実行できます)
10 年ほど前にネット上で「chicken はインタープリターを作っているチームと、コンパイラーを作っているチームが別らしい。両者の挙動が違っていて具合が悪い」という書き込みを見た記憶があります。今回はそれを微妙に実感しました。ただし今回のは問題ではなく、問題の一歩か二歩手前くらいです。
そもそも、出しているのはワーニングだけ。コンパイル結果のプログラムは問題なく正常に動きます。さっきから繰り返し「微妙」と書いているのはそういう意味です。
なので判断としては、ギリギリセーフ。(基本的にオケ)
第二に、FFI、embedding いわゆる組み込みに対応しているか
3つとも対応しています。C言語に組み込み出来ます。やり方(サンプル)を chap-15.で示しています。個人的に、この項目は実用性が高いと思います。
実は本音を言いますと、なんとか FFI(embedding)が使いこなせそうだったのがこの3つなのです、笑。他の処理系は難しくて使いこなせません。
第三に、新仕様に適切に追随しているか
以前のことですが、きっちり R6 に準拠した処理系(例えば Racket )は R5 時代に組んだプログラムのかなりが動かなくなりました。
R6 は if クロースの else 部分を必須にしたのですが、もともと R5 では任意だったのです。そんな具合で、古い機能を簡単に捨てちゃう処理系は、後になって困りそうです。なので上に書いたように「R5 ベースにして R6 R7 等を追加する処理系」にしたわけです。
この3つは基本的に R5 に新機能を追加し続けているので、この先も多分大丈夫ですが、chibi の書式の制限には個人的不満があります。
今回久しぶりに複数の処理系を試しました。それで気がついたのすが最近の大半の処理系は書き方に注文が厳しいようです。一種の書式制限。
以前は任意に define define define って感じで書いていました。30 年くらい前の scheme の書物にも「scheme では set! はあまり使わない。ガベージコレクションが適切に働くので define をメインに使えば良い」という解説でした。
ところが最近の処理系は先に (let ((hogehoge )) で変数等の領域を確保して、後の方でそれを set! するようです。そういう書き方をしないとエラーで弾かれてしまいます。何とも、変な仕様。
define を多用するのはむしろ scheme 本来の使い方だったはずです。それが今になってエラーで弾くって、どうかしています。文法上間違っていなかったものを、後になってから否定する仕様は良くないと思います。
残念ながら chibi を始めとして多くの処理系がその仕様になっているようです。がっかりしました。
もしかしたら未だに「30年くらい前のプログラムでも動かす。書き方の仕様変更はやらない(R6 の指定には従わない)」という方針なのは chicken guile の2つだけなのかもしれません。
第四に、速度
これは思っていたよりずっと良い結果でした。
比較するための簡易テストは Sec-4. に示します。簡易テストなのでこの結果が絶対的真理ってわけじゃないです。
一応、結果から言うと
というものでした。余談ですが chibi の速度だと http サーバに組み込んで使うのは無理だと思います。遅すぎ。
以降、その他の項目です。
書き捨てプログラム用の言語(又はスクリプトで使われる言語)
上記参考ウェブサイトの記事中で多数ある面白い部分の一つとして「書き捨てプログラムに使いやすい言語が、一つの重要な条件」というのがあります(スクリプトで使われる言語という話も文章中にあり、似た話です)。名言だと思います。
宅サバ・宅UNIX・宅プログラミングの土俵で考えれば、言語の実用的な利用分野の大半は書き捨てプログラムでしょう。5行とか20行とか、もうちょい多いくらいのプログラム(「スクリプト」というのはこういうものを指す言葉らしいです)
ちょっとした処理をする際に、ずっと以前は UNIX のツールを使っていたと思います。awk、tr、sh その他もろもろ。
UNIX に触れたばかりの頃は「こういうツールの使い方を覚えなくてはならない」と考えていました。が、しばらくしていわゆる「スクリプト系言語」(大抵の場合はインタープリター式の言語)を使えば済むことに気がつきます。全部まとめて一つのスクリプト系言語でほぼ面倒をみれます。
そうなると、もう awk、tr、sh その他もろもろの使い方をマスターしようという気は綺麗サッパリなくなります、笑。
それで書き捨てプログラム(あるいはスクリプト系の処理)に使う場合、scheme には一つ弱点があるように思います。scheme は正規表現が言語仕様上はサポートされていません。ダブルクオーテーション(文字列)の中で正規表現が使えません。
SRFI の関数なり処理系独自の拡張等で大抵の処理系では正規表現がサポートされています。が、少なくとも guile のは使いやすく無いです。
全部の scheme 処理系を試したわけではありませんが、guile を前提にするなら正規表現を使う処理は perl Lua 等を使った方が良いです。
正規表現が必要無い場合であれば、scheme を書き捨てプログラムに使うに際して困ることはないと思います。perl、lua で出来ることは基本的に scheme でも出来ると思います。
なお、書き捨てプログラムに使いやすいというのを別の言葉で言い換えると「文法が単純明快でわかりやすい。習熟が容易である」ってことだと思います。
マニュアルは分かりやすいか
これは好みが大きく分かれる話だと思います。
個人的な感覚ですが、いずれもオンラインマニュアルはしっかり書かれています。この3つなら「自分にも使える」という気になりました。
新版をコンパイル・インストールしやすいか
うち(Debian-12、slackware-15)で試した限り3つとも簡単に新版をコンパイル・インストール出来ます。新しい版を追いかけるのは簡単です。
じゃあ、新版をどんどん追いかけても大丈夫なのか?
変な問いかけです。
最近の各ディストリを見てると python、perl、lua などの so ファイル(シェアードオブジェクトファイル)が最初から入っています。
つまりこれらの言語をパッケージに組み込んでいると思われます。有名どころだと vlc は lua を使ってるようです。
scheme のうち guile についてはもしかすると slackware 等で使っているかもしれません。多のディストリでもパッケージをインストールしていくと、そのうち guile の so ファイルが入ってくるかもしれません。しかしそこまで詳細に確認はできません。
scheme でそれ以外の処理系の so ファイルは見かけません。
そうした場合、つまり最初から OS の中に so ファイルが入っていて、そこに別バージョンをどんどん入れていって良いかという問いかけです。
これについては、旧版とは別に新版を入れても支障ないはずです。so ファイルをロードする順は /etc/ld.so.conf で制御出来ます。なので自分が組み込みに使っている関係で新版から先に読ませたいと思ったら、そのように設定出来ます。
この場合、ディストロのパッケージは旧版前提(slackware だと guile-3.0.7 )で作られていて、私が入れた新版(今なら guile-3.0.10 )を想定外に読むことになります。でも、ポイントリリース番号で3つ違うだけだなので問題が生じることは無いと思います。固定リリースのディストリなら大丈夫でしょう、多分、笑。
安心度は90%くらい。心配度は10%くらいだと思っています。(なお、CUI のサーバだと実際には guile を使ったパッケージは無いでしょう)
ただし、どうしても安心度を100%にしたいなら、常用処理系をディストリが用意している版に変える(新版は追いかけない)ことです。
同様の話として、常に100%の安心度が欲しい場合、かつpython、perl、lua 等を組み込みで使うなら新版を追いかけるのは諦めることになると思います。大抵どのディストリでもこの3つの so ファイルを最初から持っています。
結論は無しです。まとめも無しです。個別評価を足し合わせて各自ご判断ください。
以下は私の感想というより、scheme に関する世間の受けとめかたについて感じることを書きます。
scheme 処理系を選ぶときに R7 準拠のものが好まれるようです。ネットの書き込みを見てそう思います。
おそらくこれから scheme を始める方は R5 より R6、R6 より R7 が良い・・・と考えているのだと思います。が、本当にそうなんでしょうか?
ちょっと違うんじゃないかという事例を2つ上げてみます。
* まず一つ目は R5 で踏みとどまって R6 の考え方に全面移行することを拒んでいる(と見える)処理系が2つあります。chicken と guile です。
奇しくもこの2つは scheme 処理系の中で人気トップの2つです。R5 に踏みとどまっていることを好む schemer 達が多くいるってことだと思います。
* 二つ目は最近(ここ 10 年くらい)新規に処理系を作る上級のハッカーさん達は scheme をあまり選んでいないこと。15 年とか 20 年前だったら scheme を選ぶ人が多かったはずです。世の中の風(?)が変わりました。今は clojure が人気みたいです。
HTMLのプロジェクト管理的な機能を clojure は多く持っていてそこは Lisp/Scheme と clojure で全く違います。
でも一般プログラミング言語の部分はそのまま Lisp/scheme です。見てくれを少し変えただけで中身はそっくり。
なので、肝心なのは「それでも scheme を選ばず clojure を選ぶのは何故か」ってことです。
これは私の想像ですが Revised Report が引っ張っていく方向性を好まない人達が多いってことだと思います。
scheme を選ぶと Revised Report をどこまで受け入れるか個別判断が必要でしょう。でも clojure を選べばそういう判断無しに「ハッカーが作るハッカーのための言語」の道を容易に進むことが出来ます。
上の方で上げた Graham 氏の文章の中に「人気の言語を作るならハッカーに任せろ」ってニュアンスの部分があります。あれ、意味深な言葉です。
逆を読めば「学者に任せるとろくなことは無い」という意味にもとれます。Graham 氏は Revised Report の活動や方向性に批判的です。
こんな部分もあるので Revised Report のより高いバージョンを求めるのが良いとは限りません。ちょっと見る視点を変えるだけで結論は全く変わります。
少し補足しておきます。「学者に任せるとろくなことはない」というのは私の主張ではありません。「Graham 氏がそう思っているかもしれない」という私の想像です。お間違いなきように、笑。
また、学者が作った言語でも Lua のように実用性をとても重視しているものもあります。
もう一つ、補足。言語としての scheme は R6 が色々(あまり良くない意味も含めて)影響していると思います。ただし幸いなことに処理系の作者さん達はそれほど影響を受けていない事例も多そうです。guile、chicken が R6 の影響を限定的にとどめています。国産 scheme でも gauche saggitarius など有名な処理系がありますが、「R6 風の学者の言語」ではなく「実用性を求めた言語」「ハッカーのための言語」の道を進んでいる様に見えます。目指している方向性がとても良いと思います。
scheme の場合は言語として評価するのと、処理系として評価するので大きな差があります。
gauche のウェブサイトに「scheme 処理系を選ぶときは、最初に慎重に選ぶこと。選んだらそれを使い込むのが良い」というニュアンスの事が書いてあります。示唆に富んだ良いアドバイスです。
比較方法
こんなやり方です。書き込んだファイルは 500 行の文字列がソートされたものです。
この比較で分かるのはIO速度(ファイルから読んだり書き込んだりする速度)と文字列の比較関数の速さです。他の項目はこの簡易テストでは分かりません。
こんなんですが、マアマア、ちょっとした目安程度にはなると思います。
また、アルゴリズムは単純2分木とします。アルゴリズムの優劣をチェックするなら、自己調整型2分木とかB木とか他にもっと優れたものがあります。
しかし今回の比較は同一プログラムを使って処理系間の速度比較するのが目的です。なので単純なアルゴリズムにしました。
500 行の文字列を含むファイルは http サーバのアクセス記録 500 行分を使いました。
実行環境は CPU が Ryzen3400G、メモリー16GB、Debian-12、Xfce で xfce4-terminal で実行しました。
(Slackware-15 で試すと、20%くらい Debian より高速でした)
Debian での結果は以下のとおりです。
guile、chicken、chibi-scheme の3つの結果を比較しています。chicken についてはコンパイル版も示しています。guile はコンパイルされたものを自動的に使う仕組みです。
比較のために、同一アルゴリズムでつくった lua 版の結果も示しています。
キャッシュの影響を同一にするため、それぞれ3回実行して、そのうちのベスト値を掲載しました。
バージョンはいずれも 2025年初時点での最新版です。
guile の結果 $ time ./test-guile.scm < http500.log > data.guile real 0m0.044s user 0m0.032s sys 0m0.013s chicken インタープリター版の結果 $ time ./test-chicken.scm < http500.log > data.chicken real 0m0.075s user 0m0.061s sys 0m0.013s chicken コンパイル版の結果 $ time ./test-chicken2 < http500.log > data.chicken2 real 0m0.021s user 0m0.012s sys 0m0.010s chibi の結果 $ time ./test-chibi.scm < http500.log > data.chibi real 0m0.328s user 0m0.311s sys 0m0.016s 比較のために Lua の場合 $ time ./go.lua < http500.log > data.lua real 0m0.024s user 0m0.020s sys 0m0.005s
参考までに僭越ながら比較に使った scheme のプログラムです。適当にやってるので間違ってるかもしれません。間違っていたら、直して使ってください。
test パッケージ
chicken コンパイル版はインタープリター版の(load "tree03.scm") の一行を削除して、そこに直に tree03.scm のソースを埋め込みます。(全体で一つのファイルにしておく)
それを test-chicken2.scm とかして、その後、
$ csc test-chicken2.scm
とします。それで test-chicken2 という実行ファイルが出来るので、後は上記のように実行するだけです。
なお、http のログファイルは「どこの IP アドレスからどんなアクセスが来たのか・・・」など個人情報っぽい内容なので、上記 tar からは外しています。もしも自分で実行するなら、適当に文字列ファイルを調達してください。
ここから Common Lisp
Common Lisp の世界は猛者ぞろいです。私はそんな印象を持っています。
なので偉そうに処理系比較だとか、他の言語との比較とか書けません。おこがましいです。猛者・エキスパートの方々のひんしゅくをかいそうです、笑。
一方で、私の場合は利用形態にクセがあって、Lisp/scheme 系ユーザとしては例外種的なところがあるように思っています。
要するに「宅サバ、宅 UNIX、宅プログラミング」「組み込み系が好き」
なので、例外種の立場で思ったことを書きます。どっちかというと他の記事では扱っていない事柄になります。
比較は scheme 同様にやりました。アルゴリズムは単純2分木とします。
500 行の文字列を含むファイルは http サーバのアクセス記録 500 行分を使いました。scheme の時と同一のファイルです。
実行環境は CPU が Ryzen3400G、メモリー16GB、Debian-12、Xfce で xfce4-terminal で実行しました。
比較に使ったパッケージです。test2 パッケージ 基本的に scheme プログラムを Lisp に書き直しただけです。
SBCL インタープリター版 real 0m0.055s user 0m0.040s sys 0m0.016s clisp インタープリター版 real 0m0.142s user 0m0.125s sys 0m0.008s ecl インタープリター版 real 0m0.156s user 0m0.147s sys 0m0.049s clasp インタープリター版 real 0m5.616s user 0m4.777s sys 0m0.823s ccl インタープリター版 real 0m0.081s user 0m0.055s sys 0m0.019s ----------------------------------------------------------------------------- 以下、コンパイル版 SBCL コンパイル版 real 0m0.024s user 0m0.016s sys 0m0.009s clisp コンパイル版 real 0m0.058s user 0m0.031s sys 0m0.009s ecl コンパイル版 real 0m0.118s user 0m0.097s sys 0m0.041s claps コンパイル版 real 0m5.576s user 0m4.788s sys 0m0.772s ccl コンパイル版 real 0m0.051s user 0m0.037s sys 0m0.007s
clasp が異常に遅いのはスタートアップ時間が長いからです。初期立ち上がり時間。
実際の実行時間は(立ち上がってしまえば)早いのかもしれませんが、このスタートアップ時間ではとても組み込みライクな使い方が出来ません。
一例として scheme の guile の場合です。
> (define data "富士山") > (string-length data) $1 = 3
utf-8 対応なので漢字3文字(実際は9バイト)になります。
guile はこんな扱いをしていて、かつ、アスキーキャラクター、utf-8 文字、それ以外のバイナリーデータ混在でも問題なく読めます。
問題なく、その中で文字列検索も出来ます。
次々に繰り返しその処理をやらせても処理系がハングアップすることはありません。実装はとても丈夫に出来ています、笑。
次に chicken の例です。
> (define data "富士山") > (string-length data) 9
デフォルトでは utf-8 非対応。(おまじないを軽くやると utf-8 対応になります)
このデフォルトの状態でアスキーキャラクターも非アスキーキャラクター(バイナリーデータ)も問題なく読めます。
問題なく、その中で文字列検索も出来ます。
次々に繰り返しその処理をやらせても処理系がハングアップすることはありません。これも実装はとても丈夫に出来ています。
しかし gambit (scheme 処理系の一つ)は次々に同じ作業をやらせるとエラー表示をして止まってしまいます。
これはバグがあるのではなく実装として一部のエスケープシーケンス系をうまく扱えない作り方になっているからです。
なので、gambit は自分にとっては「使えない処理系」となります。別段、バグがあるとかそういうことでは無くて実装が自分の好みに合わないだけです。
この「文字列の実装が自分の好みに合うかどうか」は私にとってはとても重要な事柄です。
サーバソフトに組み込み(又は組み込みライクなこと)をして、OSに近いレイヤーで処理系を使う場合、扱うデータはアスキーとバイナリー混在です。なのでこれがうまく扱えるかどうか、私には重要なことです。
Lisp の結論を書いておくと SBCL Clisp CCL ECL を使ってみて無事に使えたのは ECL だけでした。(Clasp は処理速度が遅すぎるので対象外としました)
ECLもデフォルトではだめですがコンパイルするときに
$ ./configure --disable-unicode
とすることで事実上 chicken とよく似た状態になり、アスキーも非アスキーもエスケープシーケンス系も扱える(検索できる)ようになります。
あとの3つはだめでした。
SBCL は utf-8 非対応のコンパイルオプションがありますが、それをやるとアスキー文字しか受け付けなくなりだめです。また utf-8 対応のまま「:element-type の指定をするとバイナリーデータを扱えるようになる」とマニュアルにあったのでやってみましたが、うまく扱えませんでした。単に私のやり方がまずかっただけかもしれませんが、扱いが難しいです。
CCL も Clisp も連続してその手のデータを扱わせると、処理系が止まります。だめです。
そんなわけでサーバーソフトに組み込み(又は組み込みライク)をやれそうな common lisp 処理系は、文字列の実装を調べた限りでは ECL だけでした。
ECL 最初の試み(2025-01 頃)
smtp サーバはうまく動きます。オケです。http サーバはだめでした。
http サーバの場合、何度やってもサーバ本体が止まってしまいます。
ちなみに SBCL CCL Clisp 等を smtp サーバで使うとサーバは動いていても Lisp 処理系側が時々止まります。
それに比較すると、ECL を http サーバで使うのは逆にサーバ本体を止めてしまいます。
想像ですが、おそらく ECL の処理速度が遅いのでサーバ本体がノッキングのような状態になり(前がつまって)止まるのだろうと思われます。
数回動かそうと試みましたが、最短で30分程度、長くても2日程度でサーバ本体が停止しました。
2025-01 頃にうちの http サーバが数回停止しましたが、それは ECL のせいです、笑。
この段階で一回諦めました。
ECL 二回目の試み(2025-07 頃)
サーバ本体が止まるのは ECL と http サーバの相対速度差の問題です。
これは CPU の速度を上げても問題解決しません。両者の絶対速度が早くなるだけで相対速度差はほぼ変わらないからです。
理想を言えば Lisp 処理系を高速化すれば問題解決するはずです。が、それは無理なので反対に http サーバ側を遅らせてみます。
本来ならキャリブレーションして調整するのでしょうが、幸いに処理系速度差を上記で計測しているのでそれを利用します。
guile と ECL の二分木での速度差は約 0.07 秒でした。
二分木の処理速度差も組み込みで動かすプログラムの処理速度差も似たようなものだろうと乱暴に仮定します。それで http サーバを 0.07 秒遅延させることにしました。
やり方は簡単で、サーバ本体の http リクエストを処理する直前に usleep(0.07秒相当)を入れるだけです。やり方の説明は省略。
ちょっと意外なことに、こんないい加減な調整方法で動かしたところうまく行きます。
このセクション執筆時点で5日間無事です。
2025-01 のお試しでは最長で2日程度、早ければ30分程度でサーバダウンでしたから、今のところ最長不倒記録、笑。
なので一応ここでは「Common Lisp も組み込みで使えそうだ」ということにしておきます。
もうしばらく様子を見てから、最終結果をここに書き足すつもりです。多分、あと二週間くらい。
あと二週間うまく行ったら「使えそうだ」という書き方ではなく「使える」という書き方に変更します。
なお、遅めの処理系を使うためにサーバ本体に遅延を入れるという本末転倒なことをやっているのは、触れないでおきます、笑。
2001-07 に http サーバを運用し始めたのが Celeron 400Mhz でした。回線速度は 1Mbps。ディスクは 3.5 インチ HDD。
今は、それに比べれば概ね1000倍の速度になっています。なので http サーバにちょびっとくらい遅延を入れても問題無いです。
そのうち気が向いたら記載予定
そのうち気が向いたら記載予定
本チャプターの古い版 https://www.quinos.net/topic8/topic8.01.html