Ответ 1
tl; dr - добавить подпись типа, использовать ByteString
и включить -O3
Но, прежде всего, как уже говорили другие, вы не сравниваете одни и те же вещи, в то время как ваш c-код использует много изменчивости и систему слабых типов c. И я считаю, что запись в файл более опасна, чем эквивалент haskell. У вас есть преимущество проверки типа/вывода типа haskell.
Также обратите внимание, что без какой-либо подписи типа ваш код является полиморфным, т.е. вы можете использовать тот же код с Float
или Double
, Word8
или Int
, если хотите. Здесь лежит первая ловушка - для целых чисел GHC по умолчанию имеет значение Integer
, произвольное точное целочисленное число, эквивалентное "bigint", которое обычно медленнее на порядки.
Поэтому добавление сигнатуры типа значительно увеличивает скорость.
(для упражнений и обучения) Я сделал некоторое сравнение и реализацию с использованием unboxed типов (ub-mandel), типизированной версии (mandel) и оп тированной версии (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
- use
ByteString
- если ваш код по-прежнему не достаточно быстрый - вложите час и перейдите в распакованные типы.
- Если у вас все еще есть энергия, продолжайте читать чтение llvm и то, что делает компилятор, отправной точкой будет статья neil mitchell blog
Примечание. все эти вычисления были выполнены без использования параллельных вычислений, которые, я думаю, можно было бы сделать намного проще в 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