7

私はHaskellを学んでいて、Cでできる限り速くコードを書こうとしています。この演習では、単純な1次元物理システム用のオイラー積分器を作成しています。

  • CコードはGCC4.5.4およびでコンパイルされてい-O3ます。1.166秒で実行されます。
  • HaskellコードはGHC7.4.1とでコンパイルされてい-O3ます。21.3秒で実行されます。
  • Haskellをでコンパイルすると、 4.022-O3 -fllvmで実行されます。

それで、Haskellコードを最適化するための何かが欠けていますか?

PS:私は次の引数を使用しました:1e-8 5

Cコード:

#include <stdio.h>
double p, v, a, t;

double func(double t) {
  return t * t;
}

void euler(double dt) {
  double nt = t + dt;
  double na = func(nt);
  double nv = v + na * dt;
  double np = p + nv * dt;

  p = np;
  v = nv;
  a = na;
  t = nt;
}

int main(int argc, char ** argv) {
  double dt, limit;
  sscanf(argv[1], "%lf", &dt);
  sscanf(argv[2], "%lf", &limit);
  p = 0.0;
  v = 0.0;
  a = 0.0;
  t = 0.0;

  while(t < limit) euler(dt);
  printf("%f %f %f %f\n", p, v, a, t);
  return 0;
}

Haskellコード:

import System.Environment (getArgs)

data EulerState = EulerState !Double !Double !Double !Double deriving(Show)
type EulerFunction = Double -> Double

main = do
  [dt, l] <- fmap (map read) getArgs
  print $ runEuler (EulerState 0 0 0 0) (**2) dt l

runEuler :: EulerState -> EulerFunction -> Double -> Double -> EulerState
runEuler s@(EulerState _ _ _ t) f dt limit = let s' = euler s f dt
                                             in case t `compare` limit of
                                                  LT -> s' `seq` runEuler s' f dt limit
                                                  _ -> s'

euler :: EulerState -> EulerFunction -> Double -> EulerState
euler (EulerState p v a t) f dt = (EulerState p' v' a' t')
    where t' = t + dt
          a' = f t'
          v' = v + a'*dt
          p' = p + v'*dt
4

4 に答える 4

12

重要なポイントは、hammarとPhilipJFによってすでに言及されています。しかし、それらを集めて、それでも少し説明を加えさせてください。

上から下に進みます。

data EulerState = EulerState !Double !Double !Double !Double

タイプには厳密なフィールドがあるため、そのタイプのオブジェクトがWHNFに評価されるたびに、そのフィールドもWHNFに評価されます。この場合、それはオブジェクトが完全に評価されることを意味します。それは良いことですが、残念ながら、私たちの場合は十分ではありません。このタイプのオブジェクトは、生データをコンストラクターにアンパックする代わりに、生データへのポインターを使用して構築できます。これは、アクセラレーションフィールドに発生します(ループがタイプを直接使用しないが、パスするという事実を法として)引数としてのコンポーネント)。そのフィールドはで使用されていないためeuler

Rec {
Main.$wrunEuler [Occ=LoopBreaker]
  :: GHC.Prim.Double#
     -> GHC.Prim.Double#
     -> GHC.Types.Double
     -> GHC.Prim.Double#
     -> Main.EulerFunction
     -> GHC.Prim.Double#
     -> GHC.Prim.Double#
     -> (# GHC.Types.Double,
           GHC.Types.Double,
           GHC.Types.Double,
           GHC.Types.Double #)

そこにボックス化された引数を持つループ。つまり、各反復で、いくつかDouble#はボックス化する必要があり、いくつかはボックス化Double解除する必要があります。ボクシングとアンボクシングはそれほど費用のかかる操作ではありませんが、そうでなければタイトなループでは、多くのパフォーマンスが犠牲になる可能性があります。同じボクシング/アンボクシングの問題のさらなるインスタンスは、タイプの引数に接続されていますEulerFunction。これについては後で詳しく説明します。Philp JFによって提案され-funbox-strict-fieldsているように、または少なくとも加速フィールドのプラグマはここで役立ちますが、関数評価のボックス化/アンボックス化も削除された場合にのみ、違いが関係します。{-# UNPACK #-}

print $ runEuler (EulerState 0 0 0 0) (**2) dt l

あなたは(** 2)ここで議論として渡しています。これはCで使用する関数と同じではなく、対応するC関数はになりますreturn pow(t,2);。私のgccでは、これを使用するとCプログラムの実行時間がほぼ2倍になります(ただし、clangの場合は違いはありません)。それに関する大きなパフォーマンスの問題は、それ(**)遅い機能であるということです。(** 2)多くの引数に対して結果が異なるため\x -> x*x、そのための書き換えルールはありません。したがって、GHCのネイティブコードジェネレーターでその遅い関数を実際に取得します(それに\x -> x*xもかかわらず、2つのGHCバックエンドとclangの結果)。の代わりにパス(\x -> x*x)またはそこに渡すと、乗算が得られます(の書き換えルールがあります(^ 2)(** 2)(^ 2)7.4以降)。この時点で、私のシステムでは、NCGで生成されたコードとLLVMで生成されたコードのパフォーマンスに大きな違いはありませんが、NCGは約10%高速です。

今、大きな問題

runEuler :: EulerState -> EulerFunction -> Double -> Double -> EulerState
runEuler s@(EulerState _ _ _ t) f dt limit = let s' = euler s f dt
                                             in case t `compare` limit of
                                                  LT -> s' `seq` runEuler s' f dt limit
                                                  _ -> s'

runEuler再帰的であるため、インライン化できません。つまり、渡された関数もそこにインライン化できず、引数dtlimit引数も反復ごとに渡されます。関数をインライン化できないということは、ループ内で、関数に渡される前に引数をボックス化してから、結果をボックス化解除する必要があることを意味します。それは高価です。そしてそれは、関数の引数をインライン化した後に行うことができる最適化を行うことができないことを意味します。

hammarによって提案されたワーカー/ラッパー変換と静的引数変換をインライン化runEulerできるため、渡された関数をインライン化でき、この場合は引数のボックス化とその結果のボックス化解除を排除できます。さらに、さらに大きな影響を与えるため、この場合、関数呼び出しを削除して、1つのマシン操作に置き換えることができます。によって示されているように、それは素晴らしいタイトなループにつながります

       174,208 bytes allocated in the heap
         3,800 bytes copied during GC

と比較して

16,000,174,912 bytes allocated in the heap
     1,475,432 bytes copied during GC

オリジナルの。

一緒に、それはネイティブコードジェネレーターでCプログラムの約半分の速度を達成し、私のボックスのLLVMバックエンドで同じ速度を達成します(ネイティブコードジェネレーターはループの最適化に特に優れていませんが、LLVMはループが非常に優れているためですLLVMを介してコンパイルされた多くの言語で一般的です)。

于 2012-10-11T13:23:05.893 に答える
6

にworker-wrapper変換を適用することで、素晴らしいブーストが得られましたrunEuler

runEuler :: EulerState -> EulerFunction -> Double -> Double -> EulerState
runEuler s f dt limit = go s
  where go s@(EulerState _ _ _ t) = if t < limit then go (euler s f dt) else s

これは、ループにインライン化するのに役立ちf(おそらくCバージョンでも発生します)、多くのオーバーヘッドを取り除きます。

于 2012-10-11T03:02:05.310 に答える
5

現在、機能しているLLVMはありませんが、2倍以内に収まります。

  1. -O2GHCの代わりに使用する-O3(重要で-O3はないかと思いますが、維持されていません)
  2. 使用する-funbox-strict-fields
  3. x*x代わりにx ** 2(Cコードと同じ)を使用する
  4. 「オイラー関数」をCの場合と同じように独立した関数に移動します。

すなわち

  func :: EulerFunction
  func x = x * x

  runEuler :: EulerState -> Double -> Double -> EulerState
  runEuler s@(EulerState _ _ _ t) dt limit = let s' = euler s dt
                                             in case t `compare` limit of
                                                  LT -> s' `seq` runEuler s' dt limit
                                                  _ -> s'

  euler :: EulerState -> Double -> EulerState
  euler (EulerState p v a t) dt = (EulerState p' v' a' t')
    where t' = t + dt
          a' = func t'
          v' = v + a'*dt
          p' = p + v'*dt

あなたはおそらくそれをさらに推し進めることができます(またはおそらくDonsのようなHaskellパフォーマンスの専門家が解決策を提示します)、私はこれが生成するコアを見ていませんが、一般的にHaskellコードを「Cと同じくらい速く」する方法は「Cで書いてFFIを使ってください。」

于 2012-10-11T02:47:17.823 に答える
4

いくつかの参照:

以下は、一般的な民間伝承を表す伝道です。だから、塩の粒でそれを取ります。

C、Fortran、Ada、C ++などの先史時代の言語よりも成熟していない言語では、さまざまなマイクロベンチマーク間で安定したCのようなパフォーマンスを得ることができません。Javaでさえまだ完全には存在していません。取得することもありますが、コンパイラが失敗することもあり、GHCが頻繁に失敗します。

しかし、マイクロベンチマークはすべてを教えてくれるわけではありません。

問題は、どこでも微調整された低レベルのCコードを取得することは、大規模なプロジェクトでは経済的に実現可能ではないということです。そのため、Cプログラムは、アルゴリズム、アーキテクチャ、低レベルのボトルネックがなくなり、最終的にすべてを書き直す予定です。Cでは、低レベルのコードを調整するのは簡単ですが、大規模なアーキテクチャの変更を行うのは非常に困難です。Haskellではその逆なので、haskellとCを組み合わせて書くと、両方の長所が得られるはずです。

于 2012-10-11T11:14:52.060 に答える