为什么这个Haskell数组填充操作如此之慢?

时间:2015-01-19 23:32:12

标签: performance haskell

对于特定任务,我需要在可变数组中进行大量快速,单独的写入。为了检查性能,我使用了以下测试:

size :: Int
size = 256*256*16

arr :: UArray Int Int
arr = runST $ do
    arr <- newArray (0,size) 0 :: ST s (STUArray s Int Int)
    forM_ [0..size] $ \i -> do
        writeArray arr i i 
    unsafeFreeze arr

arr_sum = foldl' (\ sum i -> sum + (arr ! i)) 0 [0..size-1]

main = print arr_sum

结果如下:

vh:haskell apple1$ ghc -O3 bench.hs -o bench; time ./bench
Linking bench ...
549755289600

real    0m0.748s
user    0m0.697s
sys 0m0.048s

我怀疑它不应该花0.7秒来填充内存上的256 * 256 * 16阵列,所以我在JavaScript中测试了一个等效的程序:

size = 256*256*16;
x = new Array(size);
s = 0;
for (var i=0; i<size; ++i)
    x[i] = i;
for (var i=0; i<size; ++i)
    s += x[i];
console.log(s);

结果是:

vh:haskell apple1$ time node bench.js
549755289600

real    0m0.175s
user    0m0.150s
sys 0m0.024s

在C上,时间是0.012s,这是一个很好的下限。

#include <stdio.h>

#define SIZE (256*256*16)
double x[SIZE];

int main(){
    int i;
    double s = 0;
    for (i = 0; i<SIZE; ++i)
        x[i] = i;
    for (i = 0; i<SIZE; ++i)
        s += x[i];
    printf("%f",s);
};

所以这几乎证实了我的假设,即我的Haskell程序正在做其他事情,而不仅仅是填充数组并在之后对它进行求和。在某处可能存在隐藏的堆栈,但由于我使用了foldl'forM_,我无法识别它,我认为这些编译为无堆栈代码。那么,这里效率低下的根源是什么?

3 个答案:

答案 0 :(得分:5)

GHC不会像你用C完成的那样产生很好的紧密循环。根据我的经验,在运行时间中,因子为3,大约相当于该课程。

要获得更好的性能,请使用Vector库:

import qualified Data.Vector.Unboxed as V

size = 256*256*16 :: Int

doit = V.foldl' (+) 0 vec
  where vec = V.generate size id 

main = print doit

答案 1 :(得分:2)

这对评论来说太大了,但不是真的答案。你追踪的进口有点烦人,我也压制了-Wall的警告(重要的是要注意当你在看表演时):

module Main where

import Data.Array.Unboxed
import Data.Array.ST
import Data.Array.Unsafe
import Control.Monad.ST
import Control.Monad
import Data.List

size :: Int
size = 256*256*16

ar :: UArray Int Int
ar = runST $ do
    a <- newArray (0,size) 0 :: ST s (STUArray s Int Int)
    forM_ [0..size] $ \i -> do
        writeArray a i i 
    unsafeFreeze a

arrSum :: Int
arrSum = foldl' (\ s i -> s + (ar ! i)) 0 [0..size-1]

main :: IO ()
main = print arrSum

用于haskell和节点repsectively:

jberryman /tmp » time ./t         
-524288
./t  0.04s user 0.01s system 92% cpu 0.056 total
jberryman /tmp » time nodejs t.js 
549755289600
nodejs t.js  0.19s user 0.01s system 100% cpu 0.200 total

对于GHC 7.8和7.6(我必须import Data.Array.ST hiding (unsafeFreeze),基本相同的时间,但其他代码是相同的。)

编辑:糟糕,看着我不是很善于观察;请注意,在我的32位机器上,计数溢出haskell,但不是JS,所以我们有另外的苹果到橙子;这里更公平的比较可能是Integer

我绝对建议进行任何微观基准测试的标准,否则你会让自己浪费很多时间。

另外,我不相信您在C版本中初始化阵列的开销很大,所以它不是一个公平的比较。

答案 2 :(得分:2)

抱歉,我想我只能正确回答这个问题。对于任何好奇的人来说,原因与代码无关,但是当我对它们进行基准测试时,GHC没有使用-O2重新编译我自动构建的二进制文件。解决方案是使用force-rrecomp标志:

ghc -fforce-recomp -O2 bench.hs -o bench

#haskell @ freenode上的人建议的更好的解决方案是正确设置Cabal,并使用它进行构建。