一般に、融合とは、中間データ構造を取り除くことを目的とした変換を指します。あなたFuse関数呼び出しは、より効率的なものへの無駄なメモリ割り当てをもたらします。これは実際、Haskellが純粋であることの最大のアプリケーションの1つであるIMOです。そして、それを取得するために何もする必要はほとんどありません。GHCコンパイラを介して無料で提供されます。
Haskellは純粋なので、 参照透過性 と呼ばれるものが得られます。これは(リンクから)「式はどのコンテキストでも常に同じ結果に評価される」ことを意味します。1。つまり、プログラムが実際に出力する内容を変更せずに、非常に一般的なプログラムレベルの操作を実行できます。たとえば、x
、y
、z
、w
が何であるかを知らなくても、私は常にそれを知っています。
_ ((x ++ y) ++ z) ++ w
_
と同じものに評価されます
_ x ++ (y ++ (z ++ w))
_
ただし、2番目の方法では、実際にはメモリ割り当てが少なくなります(_x ++ y
_では出力リストのプレフィックス全体を再割り当てする必要があるため)。
実際、私たちが実行できるこの種の最適化はたくさんあります。Haskellは純粋なので、基本的には式全体を移動するだけです(x
、y
、z
、または上記の例のリストに評価される実際のリストまたは式の場合はw
は何も変更しません)。これはかなり機械的なプロセスになります。
さらに、高階関数の多くの同等性を思い付くことができることがわかります( 無料の定理! )。例えば、
_map f (map g xs) = map (f . g) xs
_
f
、g
、およびxs
が何であっても(2つの辺は意味的に等しい)。ただし、この式の2つの側は同じ値の出力を生成しますが、左側は常に効率が悪くなります。つまり、中間リスト_map g xs
_にスペースが割り当てられ、すぐに破棄されます。コンパイラーに、map f (map g xs)
のようなものが見つかったときはいつでも、それをmap (f . g) xs
に置き換えるように指示したいと思います。そして、GHCの場合、それは ルールの書き換え :を介して行われます。
_{-# RULES "map/map" forall f g xs. map f (map g xs) = map (f.g) xs #-}
_
f
、g
、およびxs
は、変数だけでなく、任意の式と照合できます(したがって、map (+1) (map (*2) ([1,2] ++ [3,4]))
のようなものはmap ((+1) . (*2)) ([1,2] ++ [3,4])
。( 書き換えルールを検索する良い方法はないようです なので、 リスト をコンパイルしました。 この論文 = GHC書き換えルールの動機と仕組みを説明します。
map
を最適化する方法ですか?実際、完全ではありません。上記のものは ショートカットフュージョン です。名前の種類は欠点を意味します:それはあまりうまくスケーリングせず、デバッグするのが面倒です。同じ共通関数のすべての配置に対して、大量のアドホックルールを作成する必要があります。次に、書き換えルールを繰り返し適用することで、式がうまく単純化されることを期待します。
場合によっては、書き直しルールを整理して、中間の通常の形式を作成し、その中間の形式を対象とするルールを作成することで、さらに改善できることがわかります。このようにして、書き換えルールの「ホット」パスを取得し始めます。
おそらく、これらのシステムの中で最も進んだものは ストリーム融合 共誘導シーケンス(基本的にリストのような怠惰なシーケンス)をターゲットにすることです。 この論文 および この論文 をチェックしてください(これは実際には vector
パッケージの実装方法とほぼ同じです)。たとえば、vector
では、コードは最初にStream
sとBundle
sを含む中間形式に変換され、その形式で最適化されてから、ベクトルに変換されます。
Data.Text
_?_Data.Text
_は、ストリームフュージョンを使用して、発生するメモリ割り当ての数を最小限に抑えます(これは、厳密なバリアントでは特に重要だと思います)。 source をチェックすると、「融合の対象となる」関数が実際に操作していることがわかります Stream
s ほとんどの部分(それらは一般的な形式unstream . (stuff manipulating stream) . stream
)であり、RULES
sを変換するためのStream
プラグマがたくさんあります。結局、これらの機能の任意の組み合わせが融合されることになっているため、1つの割り当てのみを実行する必要があります。
コードが融合の対象となる時期を知る唯一の実際の方法は、関連する書き換えルールを十分に理解し、GHCがどのように機能するかをよく理解することです。とは言うものの、すべきことが1つあります。可能な場合は、非再帰的な高階関数を使用してみてください。今、しかし一般的には常にもっと)簡単に融合されます。
Haskellでの融合は、書き換えルールを繰り返し適用することで発生するため、「融合」プログラム全体が元のプログラムと同じことを行うことを知って、各書き換えルールの正しさを確信するだけで十分です。プログラムの終了に関連するエッジケースがある場合を除きます。たとえば、
_ reverse (reverse xs) = xs
_
head $ reverse (reverse [1..])
はまだ終了しないので、それは明らかに真実ではありません。_head [1..]
_は終了します。 Haskell Wikiからの詳細情報 。
1 これは、これらのコンテキストで式が同じ型を維持する場合にのみ実際に当てはまります。