23

バックエンドとして LLVM を使用する言語にクロージャー (ラムダ) を追加し始めました。常にインライン化できる単純なケースのために実装しました。つまり、クロージャー定義自体のコードは、使用されている場所でインライン化されるため、生成する必要はありません。

ただし、クロージャーが常にインライン化されていない場合 (たとえば、インライン化されていない別の関数に渡される場合) にクロージャーのコードを生成する方法。好ましくは、呼び出しサイトは、通常の関数またはクロージャーが渡されるかどうかを気にせず、通常の関数として呼び出す必要があります。

合成名で関数を生成できましたが、参照環境を追加の引数として使用する必要があり、その関数を、必要な追加の引数を知らない別の関数に渡すことはできませんでした。

私は、LLVM のトランポリン組み込み関数を使用して 1 つの可能な解決策を考えました。これは、関数から単一のパラメーターを「削除」し、1 つ少ないパラメーターを受け取るトランポリン関数へのポインターを返します。この場合、クロージャー用に生成された関数が参照環境を最初のパラメーターとして受け取った場合、それを切り取って、クロージャーが実際に宣言するのとまったく同じ数のパラメーターを受け取る関数を取り戻すことができます。これは実行可能ですか?効率的?より良い解決策はありますか?

コード例:

def applyFunctionTo(value: Int, f: (Int) -> Int) = f(value)

def main() = {
  val m := 4;
  val n := 5;
  val lambda := { (x: Int) => x + m + n };
  applyFunctionTo(3, lambda)
}

ここで、これが にインライン化されず、個別にコンパイルされる可能性があり、そこで呼び出しサイトを変更できないと想像してみましょdef main() = 3 + 4 + 5applyFunctionTo。トランポリンを使用すると、生成されるコードは次のようになると思います (疑似コードで表現、* はポインターを意味します)。

def main$lambda$1(env: {m: Int, n: Int}*, x: Int) = x + env.m + env.n
def main() = {
  m = 4
  n = 5
  env* = allocate-space-for {Int, Int}
  env = {m, n}
  tramp* = create-trampoline-for(main$lambda$1*, env*)
  return applyFunctionTo(3, tramp*)
  // release memory for env and trampoline if the lambda didn't escape
}

これは正しいと思いますか?

4

2 に答える 2

8

実行可能で効率的に聞こえます。

トランポリンを必要としない別の方法は、関数ポインターと環境へのポインター、つまりスタックポインターのペアとしてクロージャータイプを定義することです。C 呼び出し規則では、余分な引数は無視されるため、最後の引数として環境を指定すると、通常の関数のコールバックとして (function_ptr, null) を使用することもできます。

于 2012-01-03T10:40:02.473 に答える
1

ばかげたアイデアは、必要なデータを保持するスレッド ローカル構造体をクロージャごとに生成することです (ローカル構造体への単なるポインタ、または複数のポインタである可能性があります)。

クロージャーの作成者は、TLS 変数を設定し、その状態を「保存」する責任があります (再帰呼び出しを許可するため)。

次に、ユーザーが通常どおり関数を呼び出すと、関数が実行され、環境が使用されます。

呼び出しの後、クロージャーの作成者は元の値を TLS 変数に「復元」します。

于 2012-01-09T16:27:57.683 に答える