【问题标题】:Why does Haskell perform so poorly when executing C-like codes? (in this instance at least)为什么 Haskell 在执行类 C 代码时表现如此糟糕? (至少在这种情况下)
【发布时间】:2013-07-16 21:37:18
【问题描述】:

我正在尝试找出我在使用 Haskell 时遇到的一些性能问题。作为其中的一部分,我编写了一个小的比较程序来比较 C 和 Haskell。具体来说,我将 C 程序翻译为 Haskell,并尽可能少地进行更改。然后,Haskell 程序的速度测量部分以非常命令式的方式编写。

程序在某个范围内创建两个随机数列表,然后计算通过简单连接这些点形成的图形的积分,其中一个列表是 x 值,一个列表是 y 值。本质上就是trapezoidal rule

这是两个代码:

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 代码这么慢?怎么解决呢?

感谢您的任何回答。

【问题讨论】:

    标签: c performance haskell optimization


    【解决方案1】:

    出于好奇,我用 llvm 运行了这个:

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

    它从 60 毫秒缩短到 24 毫秒。仍然不理想。

    所以,当我想知道为什么这样的基准测试如此缓慢时,我要做的第一件事就是转储准备好的核心。也就是优化后的核心。

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

    看了一遍核心,最终找到了$wa,这里定义了迭代循环。事实证明,它进行了令人惊讶的许多索引绑定检查。看,我通常使用具有“unsafeRead”和“unsafeIndex”功能的 Data.Vector.Unboxed 来删除边界检查。这些在这里很有用。 个人觉得vector包更胜一筹。

    如果您查看 $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 的修改版本。它在大约 7 毫秒内运行。对于 good 矢量代码,我认为与 C 相比唯一的缓慢应该是由于不完整的别名分析。 https://gist.github.com/amosr/6026995

    【讨论】:

    • array 包也有unsafeReadunsafeWrite,无需为此切换到vector
    • 哦,好的。我快速看了一下,但看不到它们。显然太快了
    • @DanielFischer 我在 MArray 接口中看不到这些方法。当数组可以被任何实现Ix的元素索引时,它们真的可以实现吗?
    • @EricThoma 可从Data.Array.Base 获得。出于某种深不可测的原因,有人决定为此不使用 Haddock 文档,并且不从另一个已记录的模块中导出它们。我倾向于忘记它们没有记录,因为我一直在使用它们。它们都采用 Int 索引(从 0 开始)。
    【解决方案2】:

    首先,我尝试了您的代码来重现您的发现(使用 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
    • iterative 上没有工作器/包装器转换。更改只是在不需要xy 作为参数的where 子句中创建一个辅助函数。
    • Float 被使用,尽管 Double 通常表现得更好(这在这里可能无关紧要)。

    最终结果与您在 C 中发布的内容相同:

    $ ghc -O2 so.hs -hide-package random && ./so
    Computation time 11.00000 ms
    24999.048783785303
    

    【讨论】:

    • 一些小问题:迭代中的工人/包装器并不重要,因为我怀疑 SpecConstr(w/w 的泛化)无论如何都会这样做。我还认为您需要迭代中的 seq 才能正确计算时间
    • 虽然你是对的 - 以确保 w/w 发生的方式编写它可能比祈祷 SpecConstr 和其他优化做正确的事情更好
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-10
    • 1970-01-01
    • 2020-10-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多