* ES-BASIC #11
-(by [[K]], 2019.11.19)

** (13) ES-BASIC ver.0.1b のソースコード規模
-全体(すべてC言語で書かれています)
|ファイル名|行数|説明|
|''kcl03'' [川合用汎用ライブラリ]|RIGHT:1360行|高速なmalloc/free, 可変長配列, ハッシュテーブル, ソートなど|
|''kll00'' [川合用汎用言語ライブラリ]|RIGHT:890行|ストリングID, 簡易レキサー(テキストをトークン列に切り分ける), トークン列比較など|
|''bla'' [川合用汎用グラフィックライブラリ]|RIGHT:719行|小規模なグラフィックライブラリ|
|''bla2'' [川合用汎用グラフィックライブラリ]|RIGHT:105行|blaの追加ライブラリ(グラフィックキャラクタなど)|
|''chr''|RIGHT:105行|bla2用のキャラクタデータ|
|''esbasic''|RIGHT:1748行|''ES-BASIC本体''(今回は主にここを開発)|
|(合計)|RIGHT:4927行|実行ファイルサイズは64bit版が102.0KBで, 32bit版が102.5KB|

-esbasicの内訳(主要なもののみ)
|関数名|行数|説明|
|sub_~|RIGHT:245行|生成した機械語から呼ばれるための関数群。|
|loadReg|RIGHT:257行|式の記述から機械語を生成するための関数(結果をレジスタに格納)。以下の「サポートしている演算子」に対応している。|
|||演算対象がメモリかレジスタか定数なのかで機械語を作り分けて多少は高速化しているので、ここはサポートしている演算子の種類の割には長めになっている。|
|OpCall_put|RIGHT:64行|sub_~を呼び出すためのコードを生成。主にパラメータの評価と受け渡し処理。|
|eval0|RIGHT:60行|機械語のエントリ部分のコード生成。|
|evalAdd|RIGHT:408行|指定された1行を機械語に翻訳。以下の「サポートしている命令」に対応している。結構たくさんあるのでそれなりには長い。|
|eval1|RIGHT:50行|機械語のジャンプ先の確定処理や、実行処理など。|
|eval2|RIGHT:26行|機械語生成のために使用したメモリ域などを解放。|
|cmplTimeRun0|RIGHT:26行|@cmplTimeやCONST()の実現のための記述。|
|sub_run|RIGHT:77行|上記の関数群を使ってプログラムを機械語に翻訳して実行する。デバッグのためのコードも混ぜ込む。|
|cmdlineLoop|RIGHT:115行|REPL(Read-Exec-Print-Loop)のための記述。REGS命令、LOAD命令、RESUME命令は例外的にここで処理される。|
|bla_main|RIGHT:74行|ちょっとblaライブラリの都合で名前が変だけど、要するにmain関数。主に初期化処理をしている。|
|(小計)|RIGHT:1402行|これで全体の80.2%を説明できたことになる。|

-このセクションでアピールしたいことは、ES-BASICが小規模であるということです。
-高速なスクリプト言語を作るのは、世間で思われているほどには難しくないのです。・・・っていうか難しかったら私にはできません。

** (14) ES-BASIC ver.0.1b の主な機能
-[1]かなり高速、コンパクト
--gccに近い速度が簡単に出ます。スクリプト言語(インタプリタ)としては''「規格外」''の速度です(下記のベンチマーク欄を見てください)。
--たぶんこの点が最大の特徴です。
--言語処理系は''100KB程度''であり、この規模の言語としてはコンパクトなほうだと思われます。
--もちろんスクリプト言語なので、実行ファイルを作るなどの作業を事前にする必要はなく、RUNするだけですぐに実行できます。
--なお、内部でJITコンパイラを持つことで、高速な実行と言語処理系の小ささを両立させています(JIT = Just In Time)。

-[2]基本構文
--すべての行に行番号を付けなければいけないタイプのBASIC言語です(私が好きな1980年くらいのBASICはみんなそんな感じでした)。
--一つの行に複数の文を書くことができます。その場合は、セミコロンで区切ります。
--テキストエディタなどがなくても、コンソールだけでプログラムを作ることができます。
--言語としては、静的型付き言語です(そもそもBASICはそういう言語です)。動的型付き言語だと、私の力ではスピードが出せそうにないので今回はあきらめました

-[3]サポートしている演算子
|+|-|*|/|%|<<|>>|&|||^|array[i]|
|加算|減算|乗算|除算|剰余|左シフト|右シフト|ビットAND|ビットOR|ビットXOR|配列|
--このほかにも A*:B>>C のような演算子も持っていますが、説明がめんどくさいのでひとまず省略します。
--配列アクセスは添え字が宣言の範囲外になると、実行時エラーで止まります。

-[4]サポートしている命令
|命令|ver|説明|
|A=B|01b|単純な代入文です。Bの部分は式になっていてもOKです。|
|PRINT ...|01b|式の値をコンソールに表示します。|
|FOR A=B,C ... NEXT|01b|定番の繰り返し命令です。終値の後ろに増分値を指定することもできます。|
|FORNE A=B,C ... NEXT|01b|FOR I=0,10 ... NEXT は、I=10も回りますが、FORNEだとI=9までしか回りません。|
|DOLOOP ... ENDDO 条件|01b|do~while相当のループです。条件を省略すると無限ループになります。|
|IF 条件 THEN ... ELSE ... FI|01b|定番のIF文です。ELSE節は省略可能です。|
|LABEL ラベル名|01b|GOTOやGOSUBの飛び先のためにラベルを宣言できます。|
|IF 条件 GOTO 行番号orラベル|01b|条件ジャンプです。|
|GOTO 行番号orラベル|01b|無条件ジャンプです。|
|GOSUB 行番号orラベル|01b|サブルーチンを呼び出します。|
|RETURN|01b|サブルーチンから復帰します。|
|NORETURN|01b|サブルーチンから復帰しますが、呼び出し元には帰りません。行番号orラベルを追加指定することもできて、その場合はGOTO動作も行います。|
|PUSH 式|01b|式の値をスタックに保存します。|
|POP 変数名|01b|スタックから値を取り出します。今のES-BASICにはローカル変数がないので、PUSH/POPとGOSUB/RETURNを使うと、再帰処理ができるようになります。|
|LIST|01b|プログラムを表示します。|
|RUN|01b|プログラムを実行します。|
|RENUM|01b|行番号を付け直します。BASICが好きな人には定番の命令です(笑)。|
|NEW|01b|プログラムを全行消去します。ただし変数の値は消えません。|
|EXIT|01b|ES-BASICを終了します。|
|END|01b|プログラムを正常終了します。|
|STOP|01b|プログラムを中断します。どの行で中断したのかも表示されます。再開はできません。主にエラー終了のために使います。|
|PAUSE|01b|プログラムを一時停止します。どの行で中断したのかも表示されます。RESUME命令で再開可能です。停止中に変数を見たり変更したりすることが可能です。|
|LOAD ファイル名|01b|ファイルからプログラムを読み込みます。プログラムではなくコマンド列でもOKです。|
|ALIAS|01b|変数名や予約語や演算子に別名を与えます。|
|RAND|01b|0~32767の乱数を生成します。たいていは%で適当な範囲に丸めてから使います。|
|INPUT|01b|コンソールから数値を入力します。|
|OPENWIN|01b|グラフィックウィンドウのサイズを指定します。|
|A=RGBCOL(red,green,blue)|01b|RGB値からカラーコードを取得します。r/g/bはそれぞれ0~255です。|
|GCLS|01b|グラフィックウィンドウ全体を一色で塗りつぶします。|
|SETPIX|01b|グラフィックウィンドウに点を描画します。|
|FILLRECT|01b|グラフィックウィンドウに塗りつぶした長方形を描画します。|
|DRAWRECT|01b|グラフィックウィンドウに塗りつぶさない長方形を描画します。|
|FILLOVAL|01b|グラフィックウィンドウに塗りつぶした楕円形を描画します。|
|DRAWOVAL|01b|グラフィックウィンドウに塗りつぶさない楕円形を描画します。|
|DRAWLINE|01b|グラフィックウィンドウに直線を描画します。|
|FILL|01b|グラフィックウィンドウの任意の領域を塗りつぶします。|
|GPRINTI|01b|グラフィックウィンドウ内に数値を描画します。|
|GPRINTS|01d|グラフィックウィンドウ内に文字列を描画します。|
|EWAIT|01b|指定した時間だけ待機します(sleep)。単位はミリ秒です。|
|EINKEY|01b|グラフィックウィンドウへのキー入力を得ます。何も入力がないときは0を返します。|
|FLUSHWIN|01b|描画結果を確実に画面に反映させます。たいていはこの命令を使いませんが、負荷が高いときでも画面に反映させたいときは、時々使います。|
|ECHR|01b|グラフィックウィンドウ内にキャラクタを描画します。|
|GETCHR|01b|グラフィックウィンドウ内の指定された座標に表示されているキャラクタの番号を取得します。|
|GETCOL|01b|グラフィックウィンドウ内の指定された座標に表示されているキャラクタの色を取得します。|
|CHRBOX|01b|ECHRを繰り返してキャラクタで長方形を描画します。|
|ARY INT NEW A[B]|01b|B個の要素を持つ配列Aを準備します。Bは定数式でなくても構いません。内部的には配列のリサイズもできるようになっているのですが、まだリサイズ命令を作っていません。|
|ARY INT DEL A|01b|配列Aを破棄します。|
|FF16SIN|01b|三角関数の値を計算します。角度はラジアンではなく65536が1周になるような単位で渡します。また結果は小数ではなく65536倍した結果(=固定小数)で渡されます。|
|FF16SIN|01b|三角関数の値を計算します。角度はラジアンではなく65536が1周になるような単位で渡します。また結果は小数ではなく65536倍した結果(=固定小数点)で渡されます。|
|FF16COS|01b|FF16SINのコサイン版です。|
|REGS|01b|現在のレジスタの値を簡単に確認できます。|
|CODE|01b|これは必殺技みたいな命令で、16進数列を書くとそれを機械語として実行します。インライン機械語みたいなものです。|
|||試してみたい機械語があったとき、これで試して、REGSで結果を見たりして遊びます。それ以外ではめったに使いません。機械語を間違えればすぐにES-BASICが落ちます(笑)。|
|LWAIT|01b|実行速度を指定します。-1が一番高速でいくつかのデバッグ機能は無効になります。0はデバッグ機能がすべて有効になっている中では最も高速、1以上は適当にウェイトがかかるようになります。|
|TIMEMODE|01b|1を指定すると、コマンドやプログラムの実行に要した時間が表示されるようになります。高速化のために試行錯誤したいとき、これはかなり重宝します。|
|CODEDUMP|01b|1を指定すると、コマンドやプログラムがどんな機械語になったのかを確認することができます。|
|LINECOUNTER|01b|プログラムを実行した後にこの命令を実行すると、どの行が何度実行されたのかを簡単に確認することができます。|
|||これで「ここは通ったかな?」などとPRINTデバッグする必要はなくなるし、どこが負荷が重いのかを簡単に確認することもできます。|
|NODEBUG|01b|これはプログラム内で使う命令で、1を指定すると以降の行はLWAITが0以上でもデバッグ対象にはならなくなります。0を指定すると、以降の行はまたLWAITに応じてデバッグ対象になるようになります。|
|||これで怪しくない部分を高速に実行できるようになります。|
|DEBUGTRAP 行番号orラベル|01b|この命令で指定されたサブルーチンは、デバッグトラップルーチンだと認識されるようになります。0を指定すると設定が解除されます。|
|||デバッグトラップルーチンは、各行を実行する前に必ず呼び出されるサブルーチンで、変数$LINEにこれから実行しようとする行番号が入ります。|
|||これにより、変数の値を監視したり、ある行の実行の前だけ別の処理を追加したり、NORETURNを使ってある行の実行をスキップしたり、そのほかいろいろな応用ができます。|
|||デバッグトラップルーチンの中にデバッグ対象行があると、呼び出しが無限に止まらなくなってしまうので(笑)、NODEBUG命令で対象外に指定しておく必要があります。|
|PROGSEL|01b|実はES-BASICは複数のプログラムを同時に管理することもできます。これはプログラムスロットを切り替える命令です。|
|||PAUSE中に変数をいろいろ表示したいのなら、別スロットに変数を表示するためのプログラムを書いておいて、それをRUNすればいいのです。|
|PROGRUN スロット番号|01b|PROGSELでアクティブなスロット番号を切り替えずにRUNする命令です。|
|PROGNEW スロット番号|01b|PROGSELでアクティブなスロット番号を切り替えずにNEWする命令です。|
|PROGLIST|01b|現在プログラムが格納されているスロット番号の一覧と、そこに入っているプログラムの行数を表示します。|
|PROGDEL スロット番号|01b|指定されたプログラムスロットを削除します。|
|CONST(式)|01b|式の内容をコンパイル時に計算して定数として埋め込みます。|
|||したがってCONSTの中で変数を参照していて、その変数の値をあとから変更しても、ここの値はコンパイル時(=実行開始時)から不変のままになります。|
|@CMPLTIME ~ @EXECTIME|01b|@CMPLTIMEのあとに書かれた行は、実行時ではなくコンパイル時に実行されます。@EXECTIMEまでがコンパイル時実行の対象です。|
|||その演算結果を$RETVALに格納して、以降のプログラムでCONST($RETVAL)として参照して使うのが普通です。こうすることで、C++でのconstexprみたいなことができます。|
|||ES-BASICはスクリプト言語なので、プログラムを実行して値を得ることは基本的に朝飯前です。だからこんな機能も簡単に実現できています。|
|||これにより、複雑な計算結果を定数として使いたいときに、計算を言語に任せることができます。|
|FF16SQRT|01d|固定小数点での平方根を計算します。|
|CLRKEYBUF|01d|キーバッファをクリアします。|
|KWAIT|01d|指定されたキーが押されるまで待ちます。|
|SETGMODE|01d|描画モードを指定します。|
|GETPIX|01d|画面上の点の色を取得します。|
|BITBLT|01d|配列データを画面の矩形範囲に転送します。|



** (15) 実プログラム例
-[[esbasic0010]]を見てください。

** (16) ベンチマーク
-詳細は[[esbasic0013]]を見てください。
|言語|処理系のインストールサイズ|タイプ|実行時間|GCC x64 -O3比|備考|
|GCC x64 8.1.0 -O3|RIGHT:449256KB|コンパイラ言語|RIGHT:1.115秒|RIGHT:1.00倍|これはコンパイル時間を含んでいません。ちなみにコンパイル時間は0.37秒でした(これは複数回試した最小値です)。|
|ES-BASIC ver.0.1b 64bit版|RIGHT:102KB|スクリプト言語|RIGHT:1.270秒|RIGHT:1.14倍|これはJITコンパイル時間を含んでいます。GCCに対して、1.14倍くらいの時間がかかっていることになります。|
|Ruby 2.6.4p104 x64|RIGHT:75323KB|スクリプト言語|RIGHT:76.102秒|RIGHT:68.2倍|[[esbasic0013]]にテストに使ったRubyのコードがあります。|
|Python 3.7.4|RIGHT:85771KB|スクリプト言語|RIGHT:344.891秒|RIGHT:309倍|[[esbasic0013]]にテストに使ったPythonのコードがあります。|
|Python+Numba||スクリプト言語||RIGHT:2倍?|以前類似のテストをしたことがあり、その時の感触から推定(後日測定して更新します)。|
|JavaScript(Node.js)||スクリプト言語||RIGHT:15倍?|以前類似のテストをしたことがあり、その時の感触から推定(後日測定して更新します)。|

-一般に、Rubyはこの手の計算だとGCCの30倍程度の時間がかかります。Pythonはさらにもっと遅いですが、NumbaというJITコンパイラを使うとGCCの2倍くらいまで一気に高速化されます。
--mrubyのJITコンパイラもあるので、これもいつかやってみたいです。 https://qiita.com/miura1729/items/a1828849ec8fec596e74
-Node.jsを使えばJavaScriptでも同様の計算をすることができて、GCCの15倍くらいの時間で実行できます。

** (17) どうしてそんなに速いの?
-まず、世間で有名な言語がいろいろ最適を頑張っているのは知っていますが、それらはES-BASICでは基本的に一切やりません。それをやったらコンパイル時間が長くなってしまいます。ES-BASICでは実行時間だけではなくJITコンパイル時間も短くしたいので、そんな方法は取れません。・・・そして世間の優秀なJITコンパイラたちは「Tracing JIT」みたいな技術を併用して、時間をかけて最適化しても元が取れそうな場所を「自動で」発見し、そこにたくさんの技法を適用して高速化しています。でもそんな難しいプログラムは私には書けないというか、私じゃない人が書いた方がうまいと思うので、私はそれはやりません。
-ES-BASICでは最適化をがんばらない代わりに、レジスタを直接使ってもよいことにしました。つまり、「i=1」と書けばメモリ上の変数iに1が代入されますが、「RAX=1」と書けばRAXレジスタに1が代入されるわけです。またレジスタ名がむき出しだと可読性に難があったので、ALIAS命令でレジスタ名を隠せるようにしました。「ALIAS i:RAX」と書けば、以降は「i=1」でRAX=1になるわけです。
-どの変数にどのレジスタを割り当てればいいのか、どの変数はメモリのままでいいのか、それらについては基本的に人間が勘で決めることになります。とはいえ、LINECOUNTER命令を使えば、どこをメインに考えればよいかすぐにわかりますし、ALIAS命令で割り当ては簡単に変えられて試せるので、それほど時間をかけずとも結構な速さになります。TIMEMODE命令があるので試行錯誤も効果のほどもすぐに確認できます。
-つまり言語が自動であれこれとやってくれる言語ではなく、人間が高速なプログラムを書くのを支援することに特化したのがES-BASICなのです。・・・人間は最適化のために高負荷な部分をあれこれといじっているうちに不要な変数の存在に気づいたり、ループ内で不変な演算をループの外で計算するようにしたり、そういうことを自然にやります。それはもちろんコンパイラが自動でやっていることそのものなのですが、しかし人間のセンスはコンパイラよりも上なので、最後は人間が勝つと私は思っています。つまりES-BASICは最終的には最強の言語になるはずなのです。私はそう思っているのです。

** (18) デバッグ支援機能
-ES-BASICではデバッグ支援も大事な要素です(SecHack365の教材でもあるので)。
-そもそも、一般に開発時間の4~7割はデバッグの時間になっているとも言われています。そうであるならば、言語がデバッグの支援のためにできることはないかとあれこれ試みることは、十分に理にかなっていると私は思います。

-プログラムをRUNしてみて、動作が怪しいと思ったらグラフィックウィンドウで「Shift+Home」を押します。するとすぐにPAUSEがかかるので、今どこを走っているか、変数はどうなっているかなどをPRINT命令をダイレクトコマンドで実行して確認します。納得できて続行してもいいと思えたら、RESUMEで再開します。
-そうではなく、デバッグ中にもう怪しいところの目星がついているのなら、そこにPAUSEコマンドを書いておくこともできます。そうするとブレークポイント的に機能します。

-配列で添え字を間違えて対象の変数以外を書き換えて大惨事になる(=もはやどこに原因があるのかわからなくなる)というのは、まあC言語とかではたまにやらかしてしまうことですが(ようするにポインタ系のバグ)、ES-BASICは配列アクセス時に添え字の範囲をチェックしているので、そんなミスは絶対におきません。やらかす直前にちゃんとどこでやってしまいそうになったかも含めて報告してくれます。
----
-そしてES-BASICのデバッグにおける目玉機能はなんといっても「DebugTrap」です。
-原理はとても単純で、各行を実行する直前に、指定しておいたサブルーチンを呼び出すというだけです。しかしこれにより、もし変数iが20以上になったら即座にPAUSEする、みたいなことが簡単にできます。
 9000 NODEBUG 1
 9010 LABEL TRAP
 9020 IF I>=20 THEN
 9030   PRINT "I>=20 IN ",$LINE
 9040   PAUSE
 9050 FI
 9060 RETURN
-たったこれだけで、変数iをすべての行で監視できるようになるわけです。実際、配列アクセスなどで変数がおかしな値になっているということが指摘されてわかっても、その原因がどこなのかを突き止めるのはそれなりに面倒なことです。でもこの機能を使えば簡単に見つけられるわけです。
-今回は簡単な条件でしたが、もっとややこしい条件でももちろん見つけられます。IF文を書くだけのことです。
-プログラム中に出てきた変数$LINEは特別な変数で、どこの行からGOSUBしてきたのかを確認するためのものです。これを使うと、
 9051 IF $LINE==2340 THEN PRINT "[2340] "; FI
-みたいなこともできます。これは2340行の行頭に PRINT "[2340] "; を書き足したのと同じ効果があります。
-また、
 9052 IF $LINE==3450 THEN NORETURN 3460; FI
-ということもできます。これは3450行を実行せずに3460行へ帰ることになるので、3450行をコメントアウトしたのと同じことになります。
-こうしてプログラム本体には一切手を入れずに、デバッグのための一時的な変更を記述することができるのです。

* こめんと欄
#comment

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS