5

Haskell で発生しているいくつかのパフォーマンスの問題を理解しようとしています。その一環として、C と Haskell を比較する小さな比較プログラムを作成しました。具体的には、できるだけ変更を加えずに C プログラムを Haskell に翻訳しました。Haskell プログラムの速度測定部分は、非常に命令的なスタイルで書かれています。

プログラムは、ある範囲の乱数の 2 つのリストを作成し、それらの点を単純に接続して形成されるグラフの積分を計算します。1 つのリストは x 値で、もう 1 つのリストは y 値です。本質的に、それは台形規則です。

以下に 2 つのコードを示します。

main.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define N 5000000
#define maxY 1e5f/N
#define maxXgap 1

int main(){
    int i;
    float *y, *x;
    float xaccum, area;
    clock_t begin, end;
    double time_spent;

    y = (float*)malloc(sizeof(float)*N);
    x = (float*)malloc(sizeof(float)*N);

    srand(50546345); // change seed for different numbers

    //populate y and x fields with random points
    for(i = 0; i < N; i++){
        y[i] = ((float)rand())/((float)RAND_MAX)*maxY;
    }
    xaccum = 0;
    for(i = 0; i < N; i++){
        x[i] = xaccum;
        xaccum += ((float)rand())/((float)RAND_MAX)*maxXgap;
    }
    begin = clock();
    //perform a trapezoidal integration using the x y coordinates
    area = 0;
    for(i = 0; i < N-1; i++){
        area += (y[i+1]+y[i])/2*(x[i+1]-x[i]);
    }
    end = clock();
    time_spent = (double)(end - begin) / CLOCKS_PER_SEC * 1000;
    printf("%i points\n%f area\n%f ms\n", N, area, time_spent);
}

Main.hs

{-# LANGUAGE BangPatterns #-}
module Main where

import Data.Array.Unboxed
import Data.Array.IO
import Data.List
import System.Random
import System.CPUTime
import Text.Printf
import Control.Exception

main :: IO ()
main = do
          (x,y) <- initArrays
          area <- time $ integrate x y
          print area

n :: Int
n = 5000000

maxY :: Float
maxY = 100000.0/(fromIntegral n)

maxXgap :: Float
maxXgap = 1

--initialize arrays with random floats
--this part is not measured in the running time (very slow)
initArrays :: IO (IOUArray Int Float, IOUArray Int Float)
initArrays = do
                y <- newListArray (0,n-1) (randomList maxY n (mkStdGen 23432))
                x <- newListArray (0,n-1) (scanl1 (+) $ randomList maxXgap n (mkStdGen 5462))
                return (x,y)

randomList :: Float -> Int -> StdGen -> [Float]
randomList max n gen = map (abs . ((*) max)) (take n . unfoldr (Just . random) $ gen)

integrate :: IOUArray Int Float -> IOUArray Int Float -> IO Float
integrate x y = iterative x y 0 0

iterative :: IOUArray Int Float -> IOUArray Int Float -> Int -> Float -> IO Float
iterative x y !i !accum = do if i == n-1
                              then return accum
                              else do x1 <- readArray x i
                                      x2 <- readArray x (i+1)
                                      y1 <- readArray y i
                                      y2 <- readArray y (i+1)
                                      iterative x y (i+1) (accum + (y2+y1)/2*(x2-x1))

time :: IO t -> IO t
time a = do
            start <- getCPUTime
            v <- a
            end <- getCPUTime
            let diff = (fromIntegral (end-start)) / (10^9)
            printf "Computation time %0.5f ms\n" (diff :: Double)
            return v

私のシステムでは、C 統合は約 7 ミリ秒で実行され、Haskell 統合は約 60 ミリ秒で実行されます。もちろん Haskell 版の方が遅くなりますが、なぜこんなに遅いのか不思議です。明らかに、Haskell コードには多くの非効率性があります。

Haskell コードが非常に遅いのはなぜですか? どうすれば修正できますか?

回答ありがとうございます。

4

2 に答える 2

11

好奇心から、llvm でこれを実行しました。

ghc Test.hs -O2 -XBangPatterns -fllvm -optlo-O3

60ミリ秒から24ミリ秒に短縮されました。まだ理想的ではありません。

したがって、このようなベンチマークが非常に遅い理由を知りたいときに最初に行うことの 1 つは、準備されたコアをダンプすることです。つまり、最適化後のコアです。

ghc Test.hs -O2 -ddump-prep -dsuppress-all -XBangPatterns > Test.hscore

コアを調べた後、最終的に反復ループが定義されている $wa を見つけました。驚くほど多くのインデックス バウンド チェックを行っていることがわかりました。ほら、私は通常、「unsafeRead」および「unsafeIndex」関数を持つData.Vector.Unboxedを使用して、境界チェックを削除します。これらはここで役に立ちます。個人的にはベクターパッケージの方が優れていると思います。

$wa を見ると、最初に引数のボックス化が解除されていることがわかります。

case w_s3o9 of _ { STUArray l_s3of u_s3oi ds1_s3ol ds2_s3oH ->
case l_s3of of wild2_s3os { I# m_s3oo ->
case u_s3oi of wild3_s3ot { I# n1_s3ov ->
case ds1_s3ol of wild4_s3oC { I# y1_s3oE ->

これは悪いように見えますが、再帰呼び出しでは、ボックス化されていない整数などを含む特殊なバージョンの integrate_$s$wa を使用していることが判明しました。これは良いことです。

要約すると、安全でないインデックス付けでベクターを使用することで、改善が得られるはずです。

編集: これは Data.Vector を使用した修正版です。約7msで実行されます。優れたベクトル コードの場合、C と比較して遅いのはエイリアス解析が不完全なためだと思います。 https://gist.github.com/amosr/6026995

于 2013-07-18T05:46:27.620 に答える
7

最初に、コードを試して調査結果を再現しました (GHC 7.6.3 -O2 -fllvm および gcc 4.7.2 および -O3 を使用)。

$ ./theHaskellVersion-rev1
Computation time 24.00000 ms
25008.195
[tommd@Vodka Test]$ ./theCVersion
5000000 points
25013.105469 area
10.000000 ms

そのため、目標が同等のパフォーマンス (ランタイムの 60% 削減) である場合、10 ミリ秒を目指しています。あなたのコードを見ると、次のようになります。

  • Arraysが使用されていますが、これは古くてぎこちないものです。に切り替えましたVector
  • にはワーカー/ラッパー変換はありませんiterativex変更は、パラメーターとして必要としない where 句で補助関数を作成することだけですy
  • Float多くの場合、驚くほど優れたパフォーマンスを発揮しますが、使用されDoubleます (これはおそらくここでは問題になりません)。

最終結果は、C で投稿したものと同等です。

$ ghc -O2 so.hs -hide-package random && ./so
Computation time 11.00000 ms
24999.048783785303
于 2013-07-18T06:07:25.333 に答える