[ 目次, 前節, 次節, 索引 ]

5.5.7 翻訳したコードと評価器のインターフェース



翻訳したコードを評価器計算機にロードしたりそれを走らせたりする方法はまだ説明しなかった. 積極制御評価器計算機は脚注38で指定した追加の演算を持つ以外は5.4.4節で定義した通りと仮定しよう. Scheme式を翻訳し, 結果の目的コードを評価器計算機にロードし, 計算機に評価器の大域環境でコードを走らせ, 結果を印字し, 評価器の駆動ループに入る手続き compile-and-goを実装しよう. また評価器を修正し, 解釈される式が, 解釈されるものだけでなく, 翻訳したコードも呼び出せるようにしよう. そうすれば翻訳した手続きを計算機に置き, それを呼び出すのに評価器を使うことが出来る:
(compile-and-go
 '(define (factorial n)
    (if (= n 1)
        1
        (* (factorial (- n 1)) n))))
 ;;; EC-Eval value:
ok

 ;;; EC-Eval input:
(factorial 5)
;;; EC-Eval value:
120

   評価器に翻訳したコードが扱える(例えば, 上のようにfactorial の呼出しを評価する)ようにするには, apply-kdispatch (5.4.1節)にあるコードを変更し, (合成手続きや基本手続きとも違う)翻訳した手続きを認識するようにし, 制御を翻訳したコードの入り口に直接飛び越すようにしなければならない:48


apply-dispatch
  (test (op primitive-procedure?) (reg proc))
  (branch (label primitive-apply))
  (test (op compound-procedure?) (reg proc))  
  (branch (label compound-apply))
  (test (op compiled-procedure?) (reg proc))  
  (branch (label compiled-apply))
  (goto (label unknown-procedure-type))


compiled-apply
  (restore continue)
  (assign val (op compiled-procedure-entry) (reg proc))
  (goto (reg val))
compiled-applyでのcontinueの回復に注意しょう. 評価器ではapply-dispatchで継続がスタックの最上段にあるようになっていたことを思い出そう. 他方翻訳したコードの入り口は, 継続はcontinueにあると思っている. そこで翻訳したコードを実行する前にcontinueを回復しなければならない.

   評価器計算機を起動した時, 翻訳したプログラムが走らせられるよう, 評価器計算機の先頭にbranch命令を追加し, flagレジスタが設定してあれば, 計算機に新しい入り口へ行かせる.49

  (branch (label external-entry))      ; flagが設定してあれば分岐する
read-eval-print-loop
  (perform (op initialize-stack))
  ...
external-entryは, 結果をvalに置き, (goto (reg continue))で終る命令列の場所をvalに入れて計算機を起動すると仮定する. この入り口での起動はvalの指示する場所へ飛び越す. しかしvalの値を印字し, 評価器の読込み-評価-印字ループの先頭へ行くprint-resultへ実行が戻るよう, まずcontinueに代入する.50

external-entry
  (perform (op initialize-stack))
  (assign env (op get-global-environment))
  (assign continue (label print-result))
  (goto (reg val))

   手続き定義を翻訳し, 翻訳したコードを実行し, 読込み-評価-印字ループを走らせる次の手続きが使えるので, 手続きを試すことが出来る. 翻訳したコードは, 結果をvalに置き, continueにある場所へ戻って欲しいので, 式を標的valと接続returnで翻訳する. 翻訳で出来た目的コードを, 評価器レジスタ計算機で実行可能な命令に変換するにはレジスタ計算機シミュレータ(5.2.2節)の手続きassembleを使う. 次にvalレジスタを初期化し, 命令のリストを指すようにし, 評価器が external-entryへ行くようflagを設定し, 評価器を起動する.


(define (compile-and-go expression)
  (let ((instructions
         (assemble (statements
                    (compile expression 'val 'return))
                   eceval)))
    (set! the-global-environment (setup-environment))
    (set-register-contents! eceval 'val instructions)
    (set-register-contents! eceval 'flag true)
    (start eceval)))

   5.4.4節の最後のようにスタックの監視が設定してあれば, 翻訳したコードのスタック使用を調べることが出来る.

(compile-and-go
 '(define (factorial n)
    (if (= n 1)
        1
        (* (factorial (- n 1)) n))))

(total-pushes = 0 maximum-depth = 0)
 ;;; EC-Eval value:
ok

 ;;; EC-Eval input:
(factorial 5)
(total-pushes = 31 maximum-depth = 14)
;;; EC-Eval value:
120
この例を, 5.4.4節の最後に示した同じ手続きの解釈版を使った(factorial 5)の評価と比べよう. 解釈版はプッシュ144回と, 最大スタック深さ28を必要とする. これはわれわれの翻訳戦略の最適化の結果を示す.
解釈と翻訳
本節のプログラムを使えば, 解釈と翻訳の切替え実行戦略の実験が出来る.51 解釈系は計算機を利用者のプログラムのレベルへ引き上げ, 翻訳系は利用者のプログラムを機械語レベルへ引き下げる. われわれはScheme言語(や他のプログラム言語)を, 機械語の上に作られた, 抽象の整合した一族と見ることが出来る. 解釈系は, プログラム実行のステップが, これらの抽象を使って組織化してあるので, 対話的プログラム開発と虫取りに優れており, 従ってプログラマにとりずっと理解し易い. 翻訳したコードは, プログラム実行のステップが機械語を使って組織化してあるので, ずっと高速に実行出来, 翻訳系は高レベルの抽象を切り割く最適化が楽に出来る.52

   解釈と翻訳の切替えは, 言語を新しい計算機へ 移植する別の戦略をもたらす. Lispを新しい計算機に実装したいとする. 戦略の一つは5.4節の積極制御評価器から始め, その命令を新しい計算機の命令へ翻訳する. 別の戦略は翻訳系から始め, 新しい計算機のコードを生成するように, コード生成器を変更する. この第二の戦略は, 元々のLispシステムで走っている翻訳系で任意のLispプログラムを最初に翻訳し, 実行時ライブラリの翻訳版と連結することで, それを新しい計算機で走らせるのを可能とする.53 もっとよいのは翻訳系自身を翻訳し, これを新しい計算機上で他のLispプログラムを翻訳するために走らせるのである.54 あるいは4.1節の解釈系の一つを翻訳し, 新しい計算機の上で走る解釈系を作ることも出来る.

問題 5.45


翻訳したコードの使うスタック演算を, 同じ計算に対する評価器の使うスタック演算と比べると, (スタック演算の総数を減らして)速度と(最大スタック深さを減らして)スペースの両方で翻訳系がスタックの使い方を最適化する程度を決めることが出来る. この最適化したスタックの使い方を, 同じ計算に対する特殊目的の計算機の性能と比べると, 翻訳系の品質のある種の表示が得られる.

a. 問題5.27は, 上に示した再帰的階乗手続きによるn!の計算で, 評価器が必要とするプッシュの回数と最大スタック深さをnの関数として決めよといった. 問題5.14は図5.11に示した特殊目的の階乗計算機で同じ計測をせよといった. 今度は翻訳したfactorialを使って同じ解析を行え.

   翻訳版のプッシュの回数の, 解釈版のプッシュの回数に対する比を求め, 最大スタック深さについても同じことを行え. n!を計算するのに使う演算の数と, スタック深さはnに線形なので, これらの比はnが大きくなるにつれ, 定数に近づく筈である. この定数は何か. 同様に特殊目的の計算機のスタック使用と解釈版の使用に対する比を求めよ.

   特殊目的のコード対解釈されるコードの比を, 翻訳するコード対解釈されるコードの比と比べよ. 手で加工した制御コードは, われわれの発達途上の万能目的の翻訳系の作ったものより遥かに優れているので, 特殊目的計算機は, 翻訳したコードよりも遥かに優れていることがわかる.

b. 性能において, 手で加工した版に近づくようなコードを生成するのを支援するような翻訳系の改良は何か.

問題 5.46


問題5.45のような解析を行い, 図5.12の特殊目的Fibonacci計算機を使った有効性と比較して, 木構造再帰のFibonacci手続き

(define (fib n)
  (if (< n 2)
      n
      (+ (fib (- n 1)) (fib (- n 2)))))
の翻訳の, 有効性を決めよ. (解釈する性能の計測には問題5.29を参照せよ.) Fibonacciでは使う時間資源はnに線形ではない. 従ってスタック演算の比はnと独立な極限値には近づかない.

問題 5.47


本節は積極制御評価器を修正し, 解釈するコードが翻訳した手続きを呼び出せるようにする方法を述べた. 翻訳した手続きが, 基本手続きと翻訳した手続きだけでなく, 解釈させる手続きをも同様に呼び出せるように, 翻訳系を修正する方法を示せ. それにはcompile-procedure-callを合成(解釈される)手続きを扱うよう修正する必要がある. targetlinkageの組合せをcompile-proc-applと同じように扱うことに注意しよう. 実際の手続き作用には, コードは評価器のcompound-apply入り口へ飛び越す必要がある. このラベルは, (アセンブラは, それがアセンブリするコードの参照するすべてのラベルは, そこで定義するよう要求するので) 目的コードで直接参照することは出来ない. そこで評価器計算機にこの入り口を保持するcompappというレジスタを追加し, それを初期化する命令も追加しなければならない.
  (assign compapp (label compound-apply))
  (branch (label external-entry))      ; flagが設定してあれば分岐する
read-eval-print-loop
  ...
コードをテストするには, 手続きgを呼び出す手続きfの定義から始めよ. fの定義を翻訳し, 評価器を起動するのにcompile-and-goを使え. 評価器で入力してgを定義しfを呼び出してみよ.

問題 5.48


本節で実装したcompile-and-goのインターフェースは, (評価器計算機を起動した時に)翻訳系は一回しか呼び出せず扱い難い. 翻訳系-解釈系のインターフェースを拡張し, 積極制御評価器から次のように呼び出せるcompile-and-run基本手続きを用意せよ:
;;; EC-Eval input:
(compile-and-run
 '(define (factorial n)
    (if (= n 1)
        1
        (* (factorial (- n 1)) n))))
;;; EC-Eval value:
ok

;;; EC-Eval input:
(factorial 5)
;;; EC-Eval value:
120


問題 5.49


積極制御評価器の読込み-評価-印字ループを使う代用として読込み-評価-印字ループを実行するレジスタ計算機を設計せよ. つまり計算機は式を読み込み, それを翻訳し, 結果のコードをアセンブリして実行し, 結果を印字するループを走らせなければならない. これはわれわれのシミュレートした設定では, 手続きcompileassembleを「レジスタ計算機の演算」として呼び出せるので, 走らせるのは簡単である.

問題 5.50


4.1節の超循環評価器を翻訳するのに翻訳系を使い, このプログラムをレジスタ計算機シミュレータを使って走らせよ. (一時に複数の定義を翻訳するには, 定義をbeginの中に包み込まなければならない.) 結果の解釈系は, 多レベルの解釈のため, 非常にゆっくり走るであろうが, 仕事のすべての細部が得られるので, 教育的な練習である.

問題 5.51


5.4節の積極制御評価器をCへ翻訳することで, SchemeのC(または自分で選んだ別の低レベルの言語)での初等的な実装を行え. このコードを走らせるには, 適切な記憶管理ルーチンや他の実装時支援を用意する必要がある.

問題 5.52


問題5.51の対位として, 翻訳系を修正し, Schemeの手続きをCの命令列に翻訳するようにせよ. 4.1節の超循環評価器を翻訳し, Cで書いたScheme解釈系を作れ.



48 もちろん, 翻訳した手続きは, 解釈する手続き同様に(基本ではない)合成手続きである. 積極制御評価器で使った用語との整合性から, 本節では(翻訳に対し)解釈されるという意味に「合成」を使う.

49 評価器計算機が branchで始るので, 評価器計算機を起動する前に常にflagレジスタを初期化しなければならない. 計算機を通常の読込み-評価-印字ループから起動するためには

(define (start-eceval)
  (set! the-global-environment (setup-environment))
  (set-register-contents! eceval 'flag false)
  (start eceval))
を使うことが出来る.

50 翻訳した手続きは, システムが印字しようとするオブジェクトなので, システムの印字演算user-print(4.1.4節)を修正し, 翻訳した手続き要素は印字しないようにする:
(define (user-print object)
  (cond ((compound-procedure? object)
         (display (list 'compound-procedure
                        (procedure-parameters object)
                        (procedure-body object)
                        ')))
        ((compiled-procedure? object)
         (display '))
        (else (display object))))


51 翻訳系を拡張し, 翻訳したコードが, 解釈する手続きを呼び出せるようにすれば, もっとうまいことが出来る. 問題5.47参照

52 実行の戦略とは独立に, システムを殺してよいとか違った答を出してよいとかいわずに, 利用者プログラムの実行中に出会った エラーを見つけ, 知らせて欲しいと強要すると, 相当なオーバーヘッドを被る. 例えば配列の範囲外の参照は, 実行前に参照の正当性をチェックすれば検出出来る. しかしチェックのオーバーヘッドは, 配列参照自身のコストの何倍にもなり得, プログラマはそういう検出が必要かどうか決めるのに, 速さと安全を比べるべきである. 優れた翻訳系は, そういう検出のついたコードを作り出すことが出来るべきであり, 冗長な検出を避けるべきであり, プログラマに翻訳したコードのエラー検出の程度や種類の制御を許すべきである.

   CやC++のような人気のプログラム言語の翻訳系は, 走っているコードにエラー検出演算を殆んど入れず, 出来るだけ高速で走らせる. その結果, エラー検出を陽に用意するのはプログラマに任せる. 困ったことに速度が制約でない危険なプログラムでも, 人々はこれを忘れ, そのプログラムは高速で危険な生涯を歩む. 例えば1988年にインターネットを麻痺させた悪名の 「ワーム」は, UNIXTM操作系の, fingerデモンで入力バッファが溢れたかどうかの検出に関する欠陥をついたものである. (Spafford 1989参照)

53 もちろん解釈戦略でも翻訳戦略でも, 記憶割当て, 入出力および評価器や翻訳系の議論の中で「基本的」として来た各種の演算を, 新しい計算機用に実装しなければならない. この作業を最小にする戦略の一つは, これらの演算の出来るだけ多くをLispで書き, 新しい計算機に翻訳することである. ついにはすべては, 新しい計算機用にハンドコードした(ごみ集めや実際の計算機の基本命令の作用などの)小さいカーネルに集約される.

54 この戦略は翻訳した翻訳系を使い, プログラムを新しい計算機で翻訳したものが, 元々のLispシステムでそのプログラムを翻訳したものと同一かの検査など, 翻訳系の正しさの楽しいテストになる. 結果は微小な細部に極めて敏感なので, 相違の根源の追跡は面白いがいらいらもする.

[ 目次, 前節, 次節, 索引 ]