为什么这个Haskell代码比C等效代码慢得多?已经使用的未装箱的矢量和刘海

时间:2016-06-20 20:17:05

标签: performance haskell

我在Haskell和C中编写了一个简短的Mandelbrot Set生成器,发现C版本比Haskell版本运行 20x 更快。虽然我预计Haskell会变慢,但考虑到我已经使用了未装箱的矢量和刘海来避免过度的篡改,我没想到会超过一个数量级。

分析显示大部分时间花在以下代码的go函数上,这实际上只是一个循环,包含一些比较,乘法和加法。

orbits limit radius a b = go 0 0 0
  where
    r2 = radius * radius
    go !n !x !y
      | n == limit    = n
      | x2 + y2 >= r2 = n
      | otherwise     = go (n + 1) (x2 - y2 + a) (2 * x * y + b)
      where
        x2 = x * x
        y2 = y * y

在执行过程中,它需要运行C代码 0.9 秒,它需要等效的Haskell代码 18 秒。它们都实现了相同的算法,它们都生成相同的输出(PGM图形文件)。

Haskell源代码在这里:

C代码在这里:

可能导致性能差异的问题是什么?

1 个答案:

答案 0 :(得分:11)

tl; dr - 添加类型签名,使用ByteString并启用-O3

但首先 - 正如其他人之前所说的那样 - 你不是在比较相同的东西,而你的c代码使用了很多可变性和c的弱类型系统。而且我相信写入文件比haskell等效文件更不安全。您可以使用haskell的类型检查/类型推断。

另请注意,如果没有任何类型签名,您的代码就是多态的 - 即如果您愿意,可以使用与FloatDoubleWord8Int相同的代码这样做。这里有第一个陷阱 - 对于整数,GHC默认为Integer,是一个任意精度整数,相当于" bigint",它通常比数量级慢。

因此,添加类型签名会极大地提高速度。

(用于锻炼和学习)我使用未装箱的类型(ub-mandel),打字版本(mandel)和op的无类型版本(ut-mandel)进行了一些比较和实现,以及c版本(c-mandel)。

测量这些程序,你得到以下(在使用Linux的现代笔记本电脑上)

★ time ./c-mandel
./c-mandel  0,46s user 0,00s system 99% cpu 0,467 total

★ time stack exec -- ut-mandel
stack exec -- ut-mandel  9,33s user 0,09s system 99% cpu 9,432 total

★ time stack exec -- mandel
stack exec -- mandel  1,70s user 0,04s system 99% cpu 1,749 total

★ time stack exec -- ub-mandel
stack exec -- ub-mandel  1,25s user 0,08s system 98% cpu 1,343 total

显然,c代码胜过所有实现 - 但只是添加类型签名会使速度提高5.49倍。虽然迁移到未装箱的类型(我不得不承认这是第一次)会带来另外36%的加速(注意:这种加速是以代码的可读性为代价的)。

但是仍然可以改进从String版本到ByteString的转换让我们更进一步。

★ time stack exec -- ub-mandel-bytestring
stack exec -- ub-mandel-bytestring 0,84s user 0,04s system 98% cpu 0,890 total

经验教训

  • 启用类型签名
  • 开启-O3
  • 使用Bytestring
  • 如果您的代码仍然不够快 - 投入一小时并转移到未装箱类型
  • 如果你仍然精力充沛,请阅读阅读llvm输出以及编译器的作用,起点为neil mitchell's blog article

注意:所有这些计算都是在没​​有使用并行计算的情况下完成的,我认为在haskell中可以比在C中更容易完成。但这是我留给别人的任务,或者查看gh: simonmar/parconc-examples获取在gpu上并行运行的版本。

为了完整性'为unboxed,bytestring版本:

Main.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE MagicHash #-}

module Main where

import Control.Monad
import Data.ByteString.Char8 as C
import System.IO (withFile, IOMode(WriteMode), Handle)

import           GHC.Prim
import           GHC.Exts (Int(..), Double(..))
import qualified Data.Vector.Unboxed as U
import qualified MandelV as MV

savePgm :: Int -> Int -> Int -> U.Vector Int -> String -> IO ()
savePgm w h orbits v filename =
  withFile filename WriteMode $ \f -> do
    hPutStrLn f "P2"
    hPutStrLn f $ C.pack $ show w ++ " " ++ show h
    hPutStrLn f (C.pack $ show orbits)
    U.imapM_ (elm f) v
  where
      elm :: Handle -> Int -> Int -> IO ()
      elm f ix e =
          if rem ix w == 0
             then hPutStrLn f $ C.pack $ show e
             else hPutStr f $ C.pack $ show e ++ " "

main :: IO ()
main = do
  let w        = 2560#  :: Int#
      h        = 1600#  :: Int#
      x1       = -2.0## :: Double#
      y1       = -1.5## :: Double#
      x2       = 1.0##  :: Double#
      y2       = 1.5##  :: Double#
      filename = "test_hs.pgm"
      orbits   = 63#    :: Int#
      radius   = 2.0##  :: Double#

      v = MV.mandelbrot orbits radius x1 y1 x2 y2 w h :: U.Vector Int
  savePgm (I# w) (I# h) (I# orbits) v filename

MandelV.hs

{-# LANGUAGE MagicHash #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE UnboxedTuples #-}

module MandelV where

import GHC.Prim
import GHC.Exts
import qualified Data.Vector.Unboxed as U

orbits :: Int# -> Double# -> Double# -> Double# -> Int#
orbits limit radius a b =
    go 0# 0.0## 0.0##
  where
    r2 = radius *## radius
    go  ::  Int# -> Double# -> Double# -> Int#
    go !n !x !y
      | unsafeCoerce# (n ==# limit)       = n
      | unsafeCoerce# (x2 +## y2 >=## r2) = n
      | otherwise                         = go (n +# 1#) (x2 -## y2 +## a) (2.0## *## x *## y +## b)
      where
        x2 = x *## x
        y2 = y *## y

mandelbrot :: Int# -> Double# -> Double# -> Double# -> Double# -> Double# -> Int# -> Int# -> U.Vector Int
mandelbrot limit radius x1 y1 x2 y2 w h = U.generate (I# (w *# h)) f
  where
    mx = (x2 -## x1) /## int2Double# (w -# 1#)
    my = (y2 -## y1) /## int2Double# (h -# 1#)
    f :: Int -> Int
    f (I# ix) = I# (orbits limit radius x y)
      where (# j,i #) = quotRemInt# ix w
            x          = mx *## (x1 +## int2Double# i)
            y          = my *## (y1 +## int2Double# j)

的相关部分

mandel.cabal

executable ub-mandel
  main-is:             Main.hs
  other-modules:       MandelV
  -- other-extensions:    
  build-depends:       base >=4.8 && <4.9
               ,       vector >=0.11 && <0.12
               ,       ghc-prim
               ,       bytestring
  hs-source-dirs:      unboxed
  default-language:    Haskell2010
  ghc-options:         -O3