12

私は、いくつかの「問題のある」プロパティを持つ関数型言語を実行するために、中間言語と仮想マシンに取り組んでいます。

  • レキシカル名前空間 (クロージャ)
  • 動的に増加するコール スタック
  • 遅い整数型 (bignums)

中間言語はスタック ベースで、現在の名前空間の単純なハッシュ テーブルを使用します。どのように見えるかを理解するために、McCarthy91関数を次に示します。

# McCarthy 91: M(n) = n - 10 if n > 100 else M(M(n + 11))
.sub M
    args
    sto n

    rcl n
    float 100
    gt
    .if
        .sub
            rcl n
            float 10
            sub
        .end
        .sub
            rcl n
            float 11
            add
            list 1
            rcl M
            call-fast
            list 1
            rcl M
            tail
        .end
    call-fast
.end

「大きなループ」は簡単です。

  • 命令をフェッチする
  • 命令ポインタ(またはプログラムカウンタ)をインクリメントします
  • 命令を評価する

に加えてstorcl関数呼び出しには 3 つの命令があります。

  • call名前空間をコピー (ディープ コピー) し、命令ポインタをコール スタックにプッシュします。
  • call-fastは同じですが、浅いコピーのみを作成します
  • tail基本的には「goto」です

実装は実に簡単です。より良いアイデアを提供するために、「大きなループ」の途中からのランダムなスニペットを次に示します (更新、以下を参照)。

    } else if inst == 2 /* STO */ {
        local[data] = stack[len(stack) - 1]
        if code[ip + 1][0] != 3 {
            stack = stack[:len(stack) - 1]
        } else {
            ip++
        }
    } else if inst == 3 /* RCL */ {
        stack = append(stack, local[data])
    } else if inst == 12 /* .END */ {
        outer = outer[:len(outer) - 1]
        ip = calls[len(calls) - 1]
        calls = calls[:len(calls) - 1]
    } else if inst == 20 /* CALL */ {
        calls = append(calls, ip)
        cp := make(Local, len(local))
        copy(cp, local)
        outer = append(outer, &cp)
        x := stack[len(stack) - 1]
        stack = stack[:len(stack) - 1]
        ip = x.(int)
    } else if inst == 21 /* TAIL */ {
        x := stack[len(stack) - 1]
        stack = stack[:len(stack) - 1]
        ip = x.(int)

問題は次のとおりです。McCarthy91 を -10000 の値で 16 回呼び出すと、ほとんど違いはありませんが、3 秒かかります (ディープコピーを最適化した後、1 秒近く追加されます)。

私の質問は、この種の言語の解釈を最適化するための一般的な手法は何ですか? ぶら下がっている果物はありますか?

リストにスライス (引数、さまざまなスタック、名前空間のマップのスライスなど) を使用したので、この種のことをあらゆる場所で行いますcall_stack[:len(call_stack) - 1]。今のところ、どのコードがこのプログラムを遅くしているのか、まったくわかりません。私は主に一般的な最適化戦略を探していますが、ヒントをいただければ幸いです。

余談:

呼び出し規則を回避することで、実行時間をかなり短縮できます。命令はスタックのlist <n>n 個の引数をフェッチし、それらのリストをスタックにプッシュし、args命令はそのリストからポップして、各項目をスタックにプッシュします。これは、第一に、関数が正しい数の引数で呼び出されていることを確認し、第二に、可変引数リスト (つまり ) で関数を呼び出せるようにするため(defun f x:xs)です。それを削除し、sto* <x>を置き換える命令を追加するとsto <x>; rcl <x>、2 秒に短縮できます。まだ華麗ではありません、とにかくこれlist/argsビジネスを持たなければなりません. :)

もう1つ(これは私が知っている長い質問です、申し訳ありません):

pprof を使用してプログラムをプロファイリングしても、ほとんどわかりませんでした (明らかでない場合に備えて、私は Go を初めて使用します) :-)。これらは、pprof によって報告された上位 3 項目です。

  16   6.1%   6.1%       16   6.1% sweep               pkg/runtime/mgc0.c:745
   9   3.4%   9.5%        9   3.4% fmt.(*fmt).fmt_qc   pkg/fmt/format.go:323
   4   1.5%  13.0%        4   1.5% fmt.(*fmt).integer  pkg/fmt/format.go:248

これまでに行った変更は次のとおりです。

  • ハッシュテーブルを削除しました。代わりに、配列へのポインターだけを渡すようになり、必要な場合にのみローカル スコープを効率的にコピーします。
  • 命令名を整数のオペコードに置き換えました。以前は、文字列の比較にかなりの時間を費やしていました。
  • 命令はなくなりましたcall-fast(他の変更の後、スピードアップはもはや測定できませんでした)
  • 「int」、「float」、および「str」命令を使用する代わりに、1 つだけ使用evalし、コンパイル時に定数を評価します (つまり、バイトコードのコンパイル)。次にeval、それらへの参照をプッシュします。
  • のセマンティクスを変更した後.if、これらの疑似関数を取り除くことができました。と同様の暗黙の gotos とブロック セマンティクスを使用して、今.ifでは 、.elseandです。(いくつかのサンプルコード).endif.sub

レクサー、パーサー、およびバイトコード コンパイラを実装した後、速度は少し低下しましたが、それほどではありませんでした。MC(-10000) を 16 回計算すると、1.2 秒で 420 万のバイトコード命令が評価されます。生成されるコードのサンプルを次に示します(このから)。

全体はgithubにあります

4

2 に答える 2

8

最適化できるものについては、何十年にもわたる研究があります。

于 2012-06-12T12:55:25.040 に答える
5

インタープリターのさまざまな概念に対して、効率的なアルゴリズム表現が必要です。ハッシュテーブルでディープコピーを行うのはひどいアイデアのように見えますが、あなたはすでにそれを削除しているようです.

(はい、配列スライスを使用したスタックポップ操作は疑わしいようです。予想されるアルゴリズムの複雑さを実際に持っていることを確認するか、専用のデータ構造(...スタック)を使用する必要があります。私は一般的に、all- Python リストや PHP ハッシュテーブルなどのデータ構造は、この特定のユース ケースを適切に処理するように設計されているとは限らないため、この用途に適していますが、スライスはすべての状況で O(1) のプッシュおよびポップ コストを保証する可能性があります。)

環境を具体化する必要がない限り、環境を処理する最善の方法は、変数の代わりに数値インデックス (de Bruijn インデックス (最後にバインドされた変数の場合は 0)、または de Bruijn レベル (変数バインドの場合は 0) を使用することです。 This way you can keep a dynamic resized array for the environment and access is very fast. ファーストクラスのクロージャがある場合は、キャプチャする必要もありますこれはよりコストがかかります。その一部を専用の構造にコピーするか、環境全体に可変でない構造を使用する必要があります。実験だけでわかりますが、私の経験では、高速で可変な環境構造を採用し、クロージャーの構築に高いコストを支払う方が、常により多くの簿記を備えた不変の構造を持つよりも優れています。もちろん、クロージャで必要な変数のみを取得するために使用分析を行う必要があります。

最後に、アルゴリズムの選択に関連する非効率の原因を根絶すると、ホット エリアは次のようになります。

  • ガベージ コレクション (間違いなく難しいトピックです。GC の専門家になりたくない場合は、既存のランタイムの再利用を真剣に検討する必要があります)。ホスト言語の GC を使用している可能性があります (インタープリター言語のヒープ割り当ては、同じ有効期間で実装言語のヒープ割り当てに変換されます)。表示したコード スニペットでは明確ではありません。この戦略は、単純な方法で合理的に効率的なものを得るのに優れています

  • 数値の実装; 操作する整数が実際に小さい場合に効率的にするためのあらゆる種類のハックがあります。あなたの最善の策は、これに多大な労力を費やした人々の作業を再利用することです。そのため、たとえばGMP ライブラリを再利用することを強くお勧めします。また、go のmath/bigパッケージでは、bignum のホスト言語サポートを再利用することもできます。

  • インタプリタ ループの低レベルの設計。あなたのような「単純なバイトコード」を持つ言語 (Parrot バイトコードのような高レベルのセマンティクスを持つ複雑なバイトコードとは対照的に、各バイトコード命令は少数のネイティブ命令に変換されます) では、実際の「バイトコードのループとディスパッチ" コードがボトルネックになる可能性があります。if/then/else (ジャンプ テーブル) のカスケードを回避し、ホスト プロセッサの分岐予測を活用し、制御フローを簡素化するために、このようなバイトコード ディスパッチ ループを記述するための最良の方法について、かなり多くの研究が行われてきました。これはスレッド化されたコードと呼ばれ、直接スレッド化、間接スレッド化など、さまざまな (かなり単純な) さまざまな手法があります。The Structure and Performance of Efficient Interpreters in 2003, and later Context threading: 仮想マシン インタープリタのための柔軟で効率的なディスパッチ テクニック。これらの手法の利点は、かなりプロセッサに依存する傾向があるため、さまざまな可能性を自分でテストする必要があります。

STG の作業は興味深いものですが (そして、プログラミング言語の実装に関する Peyton-Jones の本は優れています)、やや遅延評価に向いています。厳密な関数型言語の効率的なバイトコードの設計に関して、私の参考文献は、ZINC マシンに関する Xavier Leroy の 1990 年の作業です: The ZINC experiment: An Economical Implementation of the ML Language は、ML 言語の実装のための画期的な作業でした。 OCaml 言語の実装でまだ使用されています: バイトコードとネイティブ コンパイラの両方がありますが、バイトコードはまだ栄光の ZINC マシンを使用しています。

于 2012-06-16T17:33:02.583 に答える