ES-BASIC #11

  • (by K, 2019.11.19)

(13) ES-BASIC ver.0.1b のソースコード規模

  • 全体(すべてC言語で書かれています)
    ファイル名行数説明
    kcl03 [川合用汎用ライブラリ]1360行高速なmalloc/free, 可変長配列, ハッシュテーブル, ソートなど
    kll00 [川合用汎用言語ライブラリ]890行ストリングID, 簡易レキサー(テキストをトークン列に切り分ける), トークン列比較など
    bla [川合用汎用グラフィックライブラリ]719行小規模なグラフィックライブラリ
    bla2 [川合用汎用グラフィックライブラリ]105行blaの追加ライブラリ(グラフィックキャラクタなど)
    chr105行bla2用のキャラクタデータ
    esbasic1748行ES-BASIC本体(今回は主にここを開発)
    (合計)4927行実行ファイルサイズは64bit版が102.0KBで, 32bit版が102.5KB
  • esbasicの内訳(主要なもののみ)
    関数名行数説明
    sub_~245行生成した機械語から呼ばれるための関数群。
    loadReg257行式の記述から機械語を生成するための関数(結果をレジスタに格納)。以下の「サポートしている演算子」に対応している。
    演算対象がメモリかレジスタか定数なのかで機械語を作り分けて多少は高速化しているので、ここはサポートしている演算子の種類の割には長めになっている。
    OpCall_put64行sub_~を呼び出すためのコードを生成。主にパラメータの評価と受け渡し処理。
    eval060行機械語のエントリ部分のコード生成。
    evalAdd408行指定された1行を機械語に翻訳。以下の「サポートしている命令」に対応している。結構たくさんあるのでそれなりには長い。
    eval150行機械語のジャンプ先の確定処理や、実行処理など。
    eval226行機械語生成のために使用したメモリ域などを解放。
    cmplTimeRun026行@cmplTimeやCONST()の実現のための記述。
    sub_run77行上記の関数群を使ってプログラムを機械語に翻訳して実行する。デバッグのためのコードも混ぜ込む。
    cmdlineLoop115行REPL(Read-Exec-Print-Loop)のための記述。REGS命令、LOAD命令、RESUME命令は例外的にここで処理される。
    bla_main74行ちょっとblaライブラリの都合で名前が変だけど、要するにmain関数。主に初期化処理をしている。
    (小計)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]サポートしている命令
    命令説明
    A=B単純な代入文です。Bの部分は式になっていてもOKです。
    PRINT ...式の値をコンソールに表示します。
    FOR A=B,C ... NEXT定番の繰り返し命令です。終値の後ろに増分値を指定することもできます。
    FORNE A=B,C ... NEXTFOR I=0,10 ... NEXT は、I=10も回りますが、FORNEだとI=9までしか回りません。
    DOLOOP ... ENDDO 条件do~while相当のループです。条件を省略すると無限ループになります。
    IF 条件 THEN ... ELSE ... FI定番のIF文です。ELSE節は省略可能です。
    LABEL ラベル名GOTOやGOSUBの飛び先のためにラベルを宣言できます。
    IF 条件 GOTO 行番号orラベル条件ジャンプです。
    GOTO 行番号orラベル無条件ジャンプです。
    GOSUB 行番号orラベルサブルーチンを呼び出します。
    RETURNサブルーチンから復帰します。
    NORETURNサブルーチンから復帰しますが、呼び出し元には帰りません。行番号orラベルを追加指定することもできて、その場合はGOTO動作も行います。
    PUSH 式式の値をスタックに保存します。
    POP 変数名スタックから値を取り出します。今のES-BASICにはローカル変数がないので、PUSH/POPとGOSUB/RETURNを使うと、再帰処理ができるようになります。
    LISTプログラムを表示します。
    RUNプログラムを実行します。
    RENUM行番号を付け直します。BASICが好きな人には定番の命令です(笑)。
    NEWプログラムを全行消去します。ただし変数の値は消えません。
    EXITES-BASICを終了します。
    ENDプログラムを正常終了します。
    STOPプログラムを中断します。どの行で中断したのかも表示されます。再開はできません。主にエラー終了のために使います。
    PAUSEプログラムを一時停止します。どの行で中断したのかも表示されます。RESUME命令で再開可能です。停止中に変数を見たり変更したりすることが可能です。
    LOAD ファイル名ファイルからプログラムを読み込みます。プログラムではなくコマンド列でもOKです。
    ALIAS変数名や予約語や演算子に別名を与えます。
    RAND0~32767の乱数を生成します。たいていは%で適当な範囲に丸めてから使います。
    INPUTコンソールから数値を入力します。
    OPENWINグラフィックウィンドウのサイズを指定します。
    A=RGBCOL(red,green,blue)RGB値からカラーコードを取得します。r/g/bはそれぞれ0~255です。
    GCLSグラフィックウィンドウ全体を一色で塗りつぶします。
    SETPIXグラフィックウィンドウに点を描画します。
    FILLRECTグラフィックウィンドウに塗りつぶした長方形を描画します。
    DRAWRECTグラフィックウィンドウに塗りつぶさない長方形を描画します。
    FILLOVALグラフィックウィンドウに塗りつぶした楕円形を描画します。
    DRAWOVALグラフィックウィンドウに塗りつぶさない楕円形を描画します。
    DRAWLINEグラフィックウィンドウに直線を描画します。
    FILLグラフィックウィンドウの任意の領域を塗りつぶします。
    GPRINTIグラフィックウィンドウ内に数値を描画します。
    GPRINTSグラフィックウィンドウ内に文字列を描画します。
    EWAIT指定した時間だけ待機します(sleep)。単位はミリ秒です。
    EINKEYグラフィックウィンドウへのキー入力を得ます。何も入力がないときは0を返します。
    FLUSHWIN描画結果を確実に画面に反映させます。たいていはこの命令を使いませんが、負荷が高いときでも画面に反映させたいときは、時々使います。
    ECHRグラフィックウィンドウ内にキャラクタを描画します。
    GETCHRグラフィックウィンドウ内の指定された座標に表示されているキャラクタの番号を取得します。
    GETCOLグラフィックウィンドウ内の指定された座標に表示されているキャラクタの色を取得します。
    CHRBOXECHRを繰り返してキャラクタで長方形を描画します。
    ARY INT NEW A[B]B個の要素を持つ配列Aを準備します。Bは定数式でなくても構いません。内部的には配列のリサイズもできるようになっているのですが、まだリサイズ命令を作っていません。
    ARY INT DEL A配列Aを破棄します。
    FF16SIN三角関数の値を計算します。角度はラジアンではなく65536が1周になるような単位で渡します。また結果は小数ではなく65536倍した結果(=固定小数)で渡されます。
    FF16COSFF16SINのコサイン版です。
    REGS現在のレジスタの値を簡単に確認できます。
    CODEこれは必殺技みたいな命令で、16進数列を書くとそれを機械語として実行します。インライン機械語みたいなものです。
    試してみたい機械語があったとき、これで試して、REGSで結果を見たりして遊びます。それ以外ではめったに使いません。機械語を間違えればすぐにES-BASICが落ちます(笑)。
    LWAIT実行速度を指定します。-1が一番高速でいくつかのデバッグ機能は無効になります。0はデバッグ機能がすべて有効になっている中では最も高速、1以上は適当にウェイトがかかるようになります。
    TIMEMODE1を指定すると、コマンドやプログラムの実行に要した時間が表示されるようになります。高速化のために試行錯誤したいとき、これはかなり重宝します。
    CODEDUMP1を指定すると、コマンドやプログラムがどんな機械語になったのかを確認することができます。
    LINECOUNTERプログラムを実行した後にこの命令を実行すると、どの行が何度実行されたのかを簡単に確認することができます。
    これで「ここは通ったかな?」などとPRINTデバッグする必要はなくなるし、どこが負荷が重いのかを簡単に確認することもできます。
    NODEBUGこれはプログラム内で使う命令で、1を指定すると以降の行はLWAITが0以上でもデバッグ対象にはならなくなります。0を指定すると、以降の行はまたLWAITに応じてデバッグ対象になるようになります。
    これで怪しくない部分を高速に実行できるようになります。
    DEBUGTRAP 行番号orラベルこの命令で指定されたサブルーチンは、デバッグトラップルーチンだと認識されるようになります。0を指定すると設定が解除されます。
    デバッグトラップルーチンは、各行を実行する前に必ず呼び出されるサブルーチンで、変数$LINEにこれから実行しようとする行番号が入ります。
    これにより、変数の値を監視したり、ある行の実行の前だけ別の処理を追加したり、NORETURNを使ってある行の実行をスキップしたり、そのほかいろいろな応用ができます。
    デバッグトラップルーチンの中にデバッグ対象行があると、呼び出しが無限に止まらなくなってしまうので(笑)、NODEBUG命令で対象外に指定しておく必要があります。
    PROGSEL実はES-BASICは複数のプログラムを同時に管理することもできます。これはプログラムスロットを切り替える命令です。
    PAUSE中に変数をいろいろ表示したいのなら、別スロットに変数を表示するためのプログラムを書いておいて、それをRUNすればいいのです。
    PROGRUN スロット番号PROGSELでアクティブなスロット番号を切り替えずにRUNする命令です。
    PROGNEW スロット番号PROGSELでアクティブなスロット番号を切り替えずにNEWする命令です。
    PROGLIST現在プログラムが格納されているスロット番号の一覧と、そこに入っているプログラムの行数を表示します。
    PROGDEL スロット番号指定されたプログラムスロットを削除します。
    CONST(式)式の内容をコンパイル時に計算して定数として埋め込みます。
    したがってCONSTの中で変数を参照していて、その変数の値をあとから変更しても、ここの値はコンパイル時(=実行開始時)から不変のままになります。
    @CMPLTIME ~ @EXECTIME@CMPLTIMEのあとに書かれた行は、実行時ではなくコンパイル時に実行されます。@EXECTIMEまでがコンパイル時実行の対象です。
    その演算結果を$RETVALに格納して、以降のプログラムでCONST($RETVAL)として参照して使うのが普通です。こうすることで、C++でのconstexprみたいなことができます。
    ES-BASICはスクリプト言語なので、プログラムを実行して値を得ることは基本的に朝飯前です。だからこんな機能も簡単に実現できています。
    これにより、複雑な計算結果を定数として使いたいときに、計算を言語に任せることができます。

(15) 実プログラム例

(16) ベンチマーク

  • 詳細はesbasic0013を見てください。
    言語処理系のインストールサイズタイプ実行時間GCC x64 -O3比備考
    GCC x64 8.1.0 -O3449256KBコンパイラ言語1.115秒1.00倍これはコンパイル時間を含んでいません。ちなみにコンパイル時間は0.37秒でした(これは複数回試した最小値です)。
    ES-BASIC ver.0.1b 64bit版102KBスクリプト言語1.270秒1.14倍これはJITコンパイル時間を含んでいます。GCCに対して、1.14倍くらいの時間がかかっていることになります。
    Ruby 2.6.4p104 x6475323KBスクリプト言語76.102秒68.2倍esbasic0013にテストに使ったRubyのコードがあります。
    Python 3.7.485771KBスクリプト言語344.891秒309倍esbasic0013にテストに使ったPythonのコードがあります。
    Python+Numbaスクリプト言語2倍?以前類似のテストをしたことがあり、その時の感触から推定(後日測定して更新します)。
    JavaScript(Node.js)スクリプト言語15倍?以前類似のテストをしたことがあり、その時の感触から推定(後日測定して更新します)。
  • 一般に、Rubyはこの手の計算だとGCCの30倍程度の時間がかかります。Pythonはさらにもっと遅いですが、NumbaというJITコンパイラを使うとGCCの2倍くらいまで一気に高速化されます。
  • 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行をコメントアウトしたのと同じことになります。
  • こうしてプログラム本体には一切手を入れずに、デバッグのための一時的な変更を記述することができるのです。

こめんと欄


コメントお名前NameLink

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2019-11-24 (日) 07:08:43 (13d)