Почему этот код Haskell намного медленнее, чем эквивалент C? Уже использованные неразорвавшиеся векторы и челки

Я написал короткий генератор Set Mandelbrot в Haskell и на C и обнаружил, что версия C работает 20x быстрее, чем версия Haskell. Хотя я ожидал, что Haskell будет медленнее, я не ожидал большего, чем на порядок, учитывая, что я уже использую unboxed vectors и bangs, чтобы избежать чрезмерного thunking.

Профилирование показывает, что большая часть времени тратится на функцию 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

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