newLISP

女房が日頃お世話になっている クックパッド、おいらも間接的にお世話になってるのは、 言うまでもありません。あんた作る人、オイラー食べる人って言う役割分担がしっかりと 確立してる訳です。

そんじゃ、オイラーの役割は? そりゃ、Lispじゃなかった、Listに決まってるでしょ。 紙の上のリストに、カッコは付いていませんが、Listと看做して、買い物へGoですよ。

今回は、クックパッドの人達の頭の中を覗いてみるって事で、 COOKPAD Engineers'Blogを見ています。 この人達は、Web屋さん? Ruby屋さん? ipadとかのアプリ開発者? ああ、ドロドロイドの ググル端末も相手にしてるんか?

よりどりみどりで、httpの波に乗ってる人達の生態が垣間見れて面白い。

オイラーは、Rubyのレールで挫折した口ですから、Web系とは一定の距離を保っていて、 JavaScriptなんて、めっそうもないと思ってます。

なんてったって、JavaScriptはブラウザーのためのアセンブラですからね。 最近、 「ECMAScript 6」が正式に承認される って事で、一層激しい、バトルが繰り広げられる事でしょう。一般ユーザーは、高みの 見物をしてればいいんです。中の人になろうなんて、考えない事ですな。

昨夜のTVで、ほや の事を取り上げていた。俗に海のパイナップルと呼ばれるやつ。 定番は、ほや酢だろうけど、ほやの炊き込みご飯とか、ほや入り石巻焼きそばとか。

クックパッドに敬意を表して ほやで検索したら、73品のレシピが出てきたぞ。 一番レシピを検索しよと思ったら、会員登録せいとな。クックパッドさんも商売だから しょうがないか。つらつら見てくと、ほや玉子なんてのがある。番組で、ほや爆弾とか 言って紹介してたやつだな。

オイラーとほやのファーストコンタクトは、東北新幹線の中だった。先輩と東北出張した時、 帰りの車中ですっかり気が緩み、酒でも呑むかとなった。つまみは何にする? 社内販売の おねーさんに勧められるものをチョイス。その中に、ホヤの干したものが入っていた。

これで、やみつきになりましたねぇ。それ以来、自宅呑み用に買い求めていたよ。そして、 仙台転勤で、本格的にほやと接するようになる。駅前にある市場で、パイナップルを 買ってきて、酢の物やらてんぷらとか食していたぞ。冷酒に最高。あーー、久しぶりに 食べたくなったぞ。

newLISP 落穂拾い実験

前回出来心でOpenBSDを入れ、釣られてnewLISPも入れてしまった。これも何かの縁。 みなさん、newLISPされてるかと思って探ってみると、

newLISPの特徴、軽くまとめ

新リスプな日々

余り人気が無いみたい。でも、オイラーにはピピィと来るものがあるので、追従実験と言うか 落穂拾い実験をやってみる。

newLispは簡易的なWebサーバーになれるそうだ。実験してみる。newlispをportsで コンパイルして、残骸を残しておいたので、そこの中のexampleに移動する。 そこを、apache風に言うとhtdocsとみなして、起動する。-dだと、ずっと居残り。-pだと、 一回接続しただけで、終了するハカナイサーバーになる。

[ob: examples]$ newlisp -http -d 8080 -w `pwd`

index.cgiはお約束。env.cgiもapacheのtestに良く使ったものなので、今回は、form.html。

[ob: ~]$ w3m http://localhost:8080/form.html
Post or Get Variables

name=This is Name
checkMe=on
color=Blue
chooseOne=Third
textField=text1%0D%0Atext2%0D%0A%0D%0A

Command line

("newlisp" "./form.cgi")

Environment Variables

STY=8894.ttyp0.ob
SERVER_SOFTWARE=newLISP/10.6.2
DOCUMENT_ROOT=/usr/ports/pobj/newlisp-10.6.2/newlisp-10.6.2/examples
LOGNAME=sakae
NEWLISPDIR=/usr/local/share/newlisp-10.6.2
HTTP_USER_AGENT=w3m/0.5.3
SHELL=/bin/ksh
TERM=screen
HTTP_HOST=localhost:8080
  :

づらづらと、printenv相当も出てきたぞ。

次なる実験は、簡単に実行ファイルが出来るって話。schemeのあの人が聞いたら羨むぞ。

コマンドラインから入力した引数を大文字にするやつ

[ob: z]$ cat hoge.lsp
;; Link example
(println (upper-case (main-args 1)))
(exit)

[ob: z]$ newlisp -x hoge.lsp fuga
[ob: z]$ chmod 755 fuga
[ob: z]$ ./fuga 'hello newlisp'
HELLO NEWLISP

で、その正体は?

[ob: z]$ ldd fuga
fuga:
        Start    End      Type Open Ref GrpRef Name
        15577000 35580000 exe  1    0   0      fuga
        0dfe0000 2dfe9000 rlib 0    1   0      /usr/lib/libm.so.9.0
        04cef000 24cf8000 rlib 0    1   0      /usr/lib/libreadline.so.4.0
        040f4000 24104000 rlib 0    1   0      /usr/lib/libncurses.so.14.0
        0e3ad000 2e3dc000 rlib 0    1   0      /usr/lib/libc.so.78.1
        076f3000 076f3000 rtld 0    1   0      /usr/libexec/ld.so
[ob: z]$ file fuga
fuga: ELF 32-bit LSB shared object, Intel 80386, version 1, for OpenBSD, dynamically linked (uses shared libs), not stripped
[ob: z]$ size fuga
text    data    bss     dec     hex
317982  8712    3384    330078  5095e

出来上がった実行ファイルサイズとnewlispサイズを何となく比べてみた。

[ob: z]$ newlisp -e '(- 373489 373412)'
77
[ob: z]$ wc hoge.lsp
       4      10      77 hoge.lsp

fugaの方が77バイト大きい。もしやと思って、hoge.lspのサイズを調べてみると、77バイト。 多分、newlispの最後にくっつけているだけじゃねぇ。manには、

       -x source target
              Link the newLISP executable with a source file to built a new
              target executable.

偉そうにLinkって書いてあるけど。。。裏を取ってから、そういう発言をしましょう。 強引にファイルの尻尾を表示してみる。

[ob: z]$ tail fuga
  :
_netService;; uppercase.lsp - Link example
(println (upper-case (main-args 1)))
(exit)

やっぱりね。と言う事は、本体とhoge.lspをcatしちゃえば同じ事。実際にやってみると、 (zzzって名前で作った)replが動いちゃって駄目だった。

何処が違うか、確認してみる。diffを使いたいんだけど、バイナリーファイルだって言われて 拒否されたんで、代わりにcmpを使いました。

[ob: z]$ cmp -l fuga zzz
323848 244  46
323849 262  46
323850   5  46
323851   0  46
323852 115 100
323853   0 100
323854   0 100
323855   0 100

ファイルの323848目から、8バイト分が違うとな。ファイルの尻尾にスクリプトが くっついているから実行ヨロってマークなんだろうな。実際にどうか実物確認。

[ob: z]$ hexdump -s 323847 -C fuga
0004f107  a4 b2 05 00 4d 00 00 00  00 00 00 00 00 00 00 00  |....M...........|
0004f117  00 00 00 00 00 00 00 00  00 28 73 65 74 20 28 67  |.........(set (g|
0004f127  6c 6f 62 61 6c 20 27 6d  6f 64 75 6c 65 29 20 28  |lobal 'module) (|
   :
[ob: z]$ hexdump -s 323847 -C zzz 
0004f107  26 26 26 26 40 40 40 40  00 00 00 00 00 00 00 00  |&&&&@@@@........|
0004f117  00 00 00 00 00 00 00 00  00 28 73 65 74 20 28 67  |.........(set (g|q
   :

差分が8バイトって事だけど、おいらのオンボロ32Bitマシンだと、2種のデータだな。 目印っぽいのがzzzに見える。一方、fugaの方は、intデータが2個だな。えと、16進数を 10進に直してみるか。

で、取り出したのは、dcというunixに備え付けのRPN計算機。こいつは、入力と出力にそれぞれ 独自に進数を設定出来る。(起動時は、入出力都も、10進数扱い)。16 i で、入力を16進数扱いに 設定してから、表示させてみる。リトルエンディアンに気をつけて、上位桁から入力。

[ob: z]$ dc
16 i
5B2A4 p
373412
4D p
77

最初のintは、373412、次は77って、ソースのサイズじゃん。最初のデータは、データを 格納してるファイルオフセットだろう。裏を取る。

[ob: z]$ hexdump -s 373411 -C fuga 
0005b2a3  00 3b 3b 20 75 70 70 65  72 63 61 73 65 2e 6c 73  |.;; uppercase.ls|
0005b2b3  70 20 2d 20 4c 69 6e 6b  20 65 78 61 6d 70 6c 65  |p - Link example|
0005b2c3  0a 28 70 72 69 6e 74 6c  6e 20 28 75 70 70 65 72  |.(println (upper|
0005b2d3  2d 63 61 73 65 20 28 6d  61 69 6e 2d 61 72 67 73  |-case (main-args|
0005b2e3  20 31 29 29 29 0a 28 65  78 69 74 29 0a 0a        | 1))).(exit)..|

次は、検証だな。ウブ15.04に舞台を移して、ソースからコンパイルした。libffi.hとreadline.hが 無いぞと言う、Linuxからの嫌がらせを跳ね除けて、無事にインストール完了。

見るべきソースは、newlisp.c。-xっていうアーギュメントを頼りに、場所を特定すると、

      if (strncmp (argv[idx], "-x", 2) == 0)
        {
          if (argc == 4)
            linkSource (argv[0], argv[idx + 1], argv[idx + 2]);
          exit (0);
        }

頭から、linkSourceを探すと、目印が出てきた。

void linkSource (char *, char *, char *);
char linkOffset[] = "&&&&@@@@";

続いて関数本体を見ると、見るに耐えない八苦の後があったので、indent を使って 整形したよ。

newlispを一気読みして、@@@@ が見つからなかった、既にリンクされてるって事で、何も しない。そうでなければ、

  offset = (ptr - buffer - 8);
  *(int *) (buffer + offset) = (int) size;
  *(int *) (buffer + offset + 4) = (int) sourceLen;
  writeFile (target, buffer, size, "w");
  readFile (source, &buffer);
  writeFile (target, buffer, sourceLen, "a");

bufferにnewlispが読み込まれた状態で、2つのデータ(size,sourceLen)を書き換え、ソースを 追加してる。以上、解析終了。じゃ、片手落ちなんで、gdbにかけてみる。題材は、先ほどの 例のようにして作った fuga。元になるnewlispは、コンパイルの最後にstripされてしまうので、 沢山あるmakefile_xxxx のうち、makefile_build で、stripを無効にしておいてから作り 直したものを使う。

軽くソースをみて、loadFileがあれなんで、

sakae@uB:~/newlisp-10.6.2$ gdb -q fuga
Reading symbols from fuga...done.
(gdb) set args 'hello world'
(gdb) b loadFile
Breakpoint 1 at 0x8053620: file newlisp.c, line 3417.
(gdb) run
Starting program: /home/sakae/newlisp-10.6.2/fuga 'hello world'

Breakpoint 1, loadFile (fileName=0x80aa3fa <linkOffset+5> "", offset=908700,
    linkFlag=1, context=0x808c653) at newlisp.c:3417
3417    {
(gdb) bt
#0  loadFile (fileName=0x80aa3fa <linkOffset+5> "", offset=908700, linkFlag=1, context=0x808c653) at newlisp.c:3417
#1  0x08053897 in loadStartup (name=<optimized out>) at newlisp.c:577
#2  0x0804b175 in main (argc=2, argv=0xbffff064) at newlisp.c:785
(gdb) c
Continuing.
HELLO WORLD
[Inferior 1 (process 2311) exited normally]

ふむ、おらましは分かった。もうすこしloadFileの中を覗くと、肝は、 evaluateStreamって事だな。そして核心は

1373              eval = evaluateExpression ((CELL *) program->contents);

後は、猿が喜びいさんで、皮を剥くように、剥いていけばいいんだな。 compileExpressionなんてのも通ってるけど、ひょっとしてJITしてる? 興味は尽きないな。

猿にらっきょう(カッコよく言うと、エシャロット)を与えると、皮の中に実が有ると思って どんどん剥いていくそうです。そして何も出てこないので怒り出すとか。オイラーは、 Lispの御本尊が、cellだと思っているので、拝んでおきましょうかね。

typedef struct        {
        UINT type;
        void * next;
        UINT aux;
        UINT contents;
} CELL;

ちょっと不思議な感じがするので、consでも追ってみれば良いかな。

(gdb) bt
#0  p_cons (params=0x893549d0) at newlisp.c:5298
#1  0x1b7ba387 in evaluateExpression (cell=0x893549b0) at newlisp.c:1520
#2  0x1b7c144f in evaluateStream (stream=0xcfbe3db0, outDevice=2, flag=0) at newlisp.c:1278
#3  0x1b7c257c in executeCommandLine (command=0x800a7f70 "(cons 1 2)", outDevice=2, cmdStream=0xcfbe421c) at newlisp.c:1227
#4  0x1b7c3116 in main (argc=1, argv=0xcfbe46a4) at newlisp.c:899

いきなりp_consなんて、どうして知った? それは、primes.hを見たからさ。このLispには、 昔ながらのcar,cdrが無い。代わりは、first,restが務めているんだけど、旧人類には寂しいな。 この登録簿をいじって、car,cdrも追加しとくか。単にaliasとして追加するだけなら、 protos.hに手を入れる必要は無いかな。

実際にやってみた。primes.hにcarなりを登録するだけ。

        {"first", p_first, 0},
        {"car", p_first, 0},

こんあ具合。

> (car '(a b c))
a
> (cdr '(a b c))
(b c)

後は、山ほどある cxxr を、init.lspに登録しとけばいいんか。

OpenBSDで、動いているnewlispにgdbからアタッチしようとしたら、権利無いと言われた。 権利はどうやって、手に入れる? man ptraceしたら書いてあった。

[ob: newlisp-10.6.2]$ sudo sysctl kern.global_ptrace=1

永久にそうしたいなら、/etc/sysctl.conf に書いておけばいいんだな。

話題は変わって、面白い機能で、libcとかが使える。

sakae@uB:~$ newlisp
newLISP v.10.6.2 32-bit on Linux IPv4/6 UTF-8 libffi, options: newlisp -h

>
(import "libc.so.6" "printf")
(printf "hello\n")

printf@B74E4130
hello
6
>

printfの返値って、出力した文字数だっけ? なお、replで複数行を入力する時は、 プロンプトの所で、一度RETを叩いてから入力。実行は、もう一度空行を入力する。 慣れないと、戸惑うな。

guiserver

リナちゃんにnewLISPを入れたついでに、OpenBSDでは動かなかった、guiserverを試してみる。 普通にLISPとかでGUIをやろうとすると、やれTcl/Tkだとか、今ならOpenGLとかだと、土台が 有りすぎて困る。

そこを一挙に解決したのがJavaで絵を書かせればいいじゃん、です。さすが、newLISPの 開発者が元SUNの人らしい。説明書では、純正Java(JRE)を使えってなってるけど、今なら Open-jre8で大丈夫でしょう。

入れて、newlisp-editを起動したら、真っ白なBOXが出てくるだけでした。そんな事ってあるの? 使ってるウィンドウマネージャをawesomeからLXDEにしたら、無事に動いた。こういう事も あるのね。

src玉には、guiserver.jarが付属してるんで、それをそのまま使えばいいんだけど、Java-dirの 中には、javaファイルが多数鎮座してた。これらからjarファイルを作るとなると、antとかmebinだったかの 悪名高いものを使わないといけないのかな?

いえいえ、そんな事はないよ。make 一発でOKみたい。

        javac -Xlint java/*.java
        mv java/*.class .
        jar cmf Manifest guiserver.jar *.class images/*.png

隠れzipコマンドで、クラスファイルと画像ファイルをまとめるだけでいいんだ。

OpenBSDで動かないの、javaファイルを探れば、何かヒントが得られるかしらん。それとも、 tcpdumpあたりを使って、どんなパケットが飛んでるか調べてみる方が簡単?

ちゃんと動いてる、ウブあたりのものと比べると、一発で判明するかな? GUI回りは 余り興味が沸かないので、気が向いたらやってみよう。

newLISP bench

newLISPって速いの? そんな疑問は普通に湧いてくる。その答えがFAQに載ってた。 Benchmarking newLISP PerlやPythonと比べているのは、そういうスクリプト族と対抗しようって魂胆だな。 そういうのは、尽きないね。

でも、言語は自由に選べばいいんでねぇ。それよりオイラーが気になるのは、OSの違いで どんな差が有るかだな。ソースからnewLISPを入れると、ベンチマークが手軽に出来るようになってる。 品質保証を兼ねたテストケースの一環で環境が提供されてるんだ。

VMWARE上に入れた各種OSで、実行してみた。使ったパソコンの石のスペックは貧乏人御用達の セルロンって事ぐらいしか知らないので、OpenBSDのdmesgの結果を載せておく。多分、この 辺は、cpuidの引き写しでしょうから、実の石を反映してると思われ。

cpu0: Intel(R) Celeron(R) CPU 900 @ 2.20GHz ("GenuineIntel" 686-class) 2.20 GHz
cpu0: FPU,V86,DE,PSE,TSC,MSR,PAE,MCE,CX8,APIC,SEP,MTRR,PGE,MCA,CMOV,PAT,PSE36,CF
LUSH,DS,ACPI,MMX,FXSR,SSE,SSE2,SS,NXE,SSE3,SSSE3,XSAVE,HV,PERF,ITSC
real mem  = 267862016 (255MB)
avail mem = 251109376 (239MB)

次の表がOSバトルの結果。試合と言えば、思い出される事が有る。LLゴング。LLって軽い言語の略ね。 一年に一度、LLを愛する者達が集まってお祭りが有るんだ。今は傍流になっちゃったPerlを 始めPythonやらRubyやらPHPの自慢とかが繰り広げられるんだ。

それぞれの言語で与えられた課題をどう書くなんてのが有って、ネットに接続してWebページを 取ってくる(だったかな)なんてのが出た。PHPはページを作るのは得意だけど、そっち方面は 全く不得手。出題者はサドかと思っちゃったぞ。

試合なんで、本当にリングに上がって戦う訳ですよ。ある年に、新木場にある試合場が会場に なりました。スチール製の折り畳み椅子に1日中座るのは苦痛でしたが、面白かったた。 今年もLL 2015が、その会場で開かれるようです。 行かれる方は、座布団等を持参された方が宜しいかと思いますよ。

OpenBSD 5.7   12027.644   4.65
FreeBSD 10.1   4825.622   1.89
Ubuntsu 15.04 11732.084   4.53
Fedora 22      4492.617   1.77
MacOSX 10.9    2580       1.00

結果は、MacOSX 10.9, 2.3GHz Intel Core i5の実行時間を基準にして、何倍遅いか比が 表示されるようになったので、実行時間を逆算して載せておいた。

ウブはみんなが喜びいさんで使うので、もっとチューニングされてるかと思ったけど、newLISPに 関しては、遅い環境なのね。 上の値は、make bench した2回目の結果を載せてあるんで、たまたま運が悪かったのかな?

そんな疑いがあろうかと、何回か測定して、平均、標準偏差と、それを正規化したのを 載せておく。qa-benchの最後を修正。統計関数が標準で付属してるのは嬉しいぞ。

(set 'ttl '())
(dotimes (n 5) (set 'ttl (cons (bench) ttl)))
(println "total: " ttl)

(module "stat.lsp")
(set 'mean (stat:mean ttl))
(set 'std  (stat:sdev ttl))
(println "mean: " mean)
(println "std:  " std)
(println "std/mean " (mul 100 (div std mean)))
(exit)

浮動小数点を含む演算は、それ専用の関数があるので、間違えない事。例えば、 mulの代わりに * を使ってもエラーにならず、黙ってZEROを返してくるぞ。

まずは、ウブで実験

sakae@uB:~/newlisp-10.6.2/qa-specific-tests$ ./my

->pretty-print
total: (5384.369000000001 5424.540999999999 5303.790999999998 5340.063 5274.904000000004)
mean: 5345.5336
std:  60.23136654439582
std/mean 1.126760601493475

ふむ、先の結果11.7秒ってのは、たまたまだったんでしょうかね。5.3秒ってのを公式記録と しておきますかね。ついでに、fedoraも測っておくか。

[sakae@fedora qa-specific-tests]$ ./my

->pretty-print
total: (4793.733000000005 4612.539999999998 4490.250000000001 4603.153000000003 4524.587000000001)
mean: 4604.852600000001
std:  117.5947625674717
std/mean 2.553713935761412

微妙にfedoraの方が速いな。でも、ばらつきはfedoraの方が大きいんで、リアルタイムOSと しては、ウブに軍配が上がるな。>そんな重箱の隅をつつくなよ。

生データの少数点以下の桁数が、無駄に大きいな。最小桁はナノ・セコンドって無意味じゃん。 丸めてしまえ。(cons (round (bench) -1) ttl) なんていう風にすると、少数点以下の一桁 だけに丸めと言うか切捨てられるぞ。

-http オプション

newLISPは簡易的なWebサーバーに成れることを上で確かめた。どういう風に実現してる? etagsを設定して、すいすいソースを飛び回ってみた。昔は、このためにglobalとかを使って わざわざ閲覧システムを作ったけど、今はこれで十分。

newlisp.cの中のexecCommandLine関数の中に、こういう部分がある。

        if (!batchMode) {
                if (logTraffic == LOG_MORE)
                        writeLog(command, TRUE);
#ifndef LIBRARY
                if (strncmp(command, "GET /", 5) == 0)
                        executeHTTPrequest(command + 5, HTTP_GET);
                else if (strncmp(command, "HEAD /", 6) == 0)
                        executeHTTPrequest(command + 6, HTTP_HEAD);
                else if (strncmp(command, "PUT /", 5) == 0)
                        executeHTTPrequest(command + 5, HTTP_PUT);
                else if (strncmp(command, "POST /", 6) == 0)
                        executeHTTPrequest(command + 6, HTTP_POST);
                else if (strncmp(command, "DELETE /", 8) == 0)
                        executeHTTPrequest(command + 8, HTTP_DELETE);
#endif
                else if (!httpMode)
                        goto EXEC_COMMANDLINE;
                return;
        }

ここで、httpコマンドを確定させて、後は、nl-web.c内へ飛び、同関数の中で、DOCUMENT_ROOT等の 環境変数をセットしてから、それぞれの処理をしてるとな。

Pythonとかだと、httpサーバーは、言語外でライブラリーで処理してるけど、こちらは 組み込んである。勿論、必須のbase64の関係も内蔵してるよ。

わざわざ組み込みにしてるのは、作者さんがWebサーバーの勉強をしたかったから? それとも、LLリングで負けないように、スループットを確保したかったから? そうだ、apache付属の ベンチマークツール ab で、apacheに試合を挑んでみるか? どうだ、やるか?

いえ、今回は、ご遠慮しときますです。