Подтвердить что ты не робот

Почему версия F # этой программы на 6 раз быстрее, чем у Haskell?

Версия Haskell (1.03s):

module Main where
  import qualified Data.Text as T
  import qualified Data.Text.IO as TIO
  import Control.Monad
  import Control.Applicative ((<$>))
  import Data.Vector.Unboxed (Vector,(!))
  import qualified Data.Vector.Unboxed as V

  solve :: Vector Int -> Int
  solve ar =
    V.foldl' go 0 ar' where
      ar' = V.zip ar (V.postscanr' max 0 ar)
      go sr (p,m) = sr + m - p

  main = do
    t <- fmap (read . T.unpack) TIO.getLine -- With Data.Text, the example finishes 15% faster.
    T.unlines . map (T.pack . show . solve . V.fromList . map (read . T.unpack) . T.words)
      <$> replicateM t (TIO.getLine >> TIO.getLine) >>= TIO.putStr

Версия F # (0,17 с):

open System

let solve (ar : uint64[]) =
    let ar' = 
        let t = Array.scanBack max ar 0UL |> fun x -> Array.take (x.Length-1) x
        Array.zip ar t

    let go sr (p,m) = sr + m - p
    Array.fold go 0UL ar'

let getIntLine() =
    Console.In.ReadLine().Split [|' '|]
    |> Array.choose (fun x -> if x <> "" then uint64 x |> Some else None)    

let getInt() = getIntLine().[0]

let t = getInt()
for i=1 to int t do
    getInt() |> ignore
    let ar = getIntLine()
    printfn "%i" (solve ar)

Вышеуказанные две программы - это решения для Проблема с максимальным объемом запасов, а время для первого тестового примера кнопки Run Code.

По какой-то причине версия F # примерно на 6 раз быстрее, но я уверен, что если бы я заменил медленные библиотечные функции императивными циклами, я мог бы ускорить ее, по крайней мере, в 3 раза и более вероятно 10x.

Можно ли улучшить версию Haskell?

Я делаю это выше для учебных целей, и в целом мне трудно понять, как писать эффективный код Haskell.

4b9b3361

Ответ 1

Если вы переключитесь на ByteString и придерживаетесь простых списков Haskell (вместо векторов), вы получите более эффективное решение. Вы также можете переписать функцию решения с помощью одного левого сложения и обходной почты и правого сканирования (1). В целом, на моей машине я добился 20-кратного повышения производительности по сравнению с вашим решением Haskell (2).

Ниже код Haskell работает быстрее, чем код F #:

import Data.List (unfoldr)
import Control.Applicative ((<$>))
import Control.Monad (replicateM_)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as C

parse :: ByteString -> [Int]
parse = unfoldr $ C.readInt . C.dropWhile (== ' ')

solve :: [Int] -> Int
solve xs = foldl go (const 0) xs minBound
    where go f x s = if s < x then f x else s - x + f s

main = do
    [n] <- parse <$> B.getLine
    replicateM_ n $ B.getLine >> B.getLine >>= print . solve . parse

<суб > 1. См. изменения для более ранней версии этого ответа, который реализует solve с помощью zip и scanr.
<Суб > 2. Веб-сайт HackerRank показывает даже большее улучшение производительности.

Ответ 2

Если бы я захотел сделать это быстро в F #, я бы избегал всех функций более высокого порядка внутри solve и просто написал императивный цикл C-стиля:

let solve (ar : uint64[]) =
  let mutable sr, m = 0UL, 0UL
  for i in ar.Length-1 .. -1 .. 0 do
    let p = ar.[i]
    m <- max p m
    sr <- sr + m - p
  sr

По моим измерениям, это на 11 раз быстрее, чем ваш F #.

Тогда производительность ограничена уровнем ввода-вывода (разбор Unicode) и разбиением строк. Это может быть оптимизировано путем чтения в буфер байта и записи лексера вручную:

let buf = Array.create 65536 0uy
let mutable idx = 0
let mutable length = 0

do
  use stream = System.Console.OpenStandardInput()
  let rec read m =
    let c =
      if idx < length then
        idx <- idx + 1
      else
        length <- stream.Read(buf, 0, buf.Length)
        idx <- 1
      buf.[idx-1]
    if length > 0 && '0'B <= c && c <= '9'B then
      read (10UL * m + uint64(c - '0'B))
    else
      m
  let read() = read 0UL
  for _ in 1UL .. read() do
    Array.init (read() |> int) (fun _ -> read())
    |> solve
    |> System.Console.WriteLine

Ответ 3

Только для записи версия F # также не оптимальна. Я не думаю, что это действительно важно на данный момент, но если люди хотели сравнить производительность, то стоит отметить, что это можно сделать быстрее.

Я не очень старался (вы можете сделать это еще быстрее, используя ограниченную мутацию, которая не противоречит природе F #), но простое изменение для использования Seq вместо Array в правильных местах (чтобы избежать выделения временных массивов) делает код примерно в 2 раза быстрее:

let solve (ar : uint64[]) =
    let ar' = Seq.zip ar (Array.scanBack max ar 0UL)    
    let go sr (p,m) = sr + m - p
    Seq.fold go 0UL ar'

Если вы используете Seq.zip, вы также можете отказаться от вызова take (потому что Seq.zip автоматически обрезает последовательность). Измеряется с помощью #time с использованием следующего фрагмента:

let rnd = Random()
let inp = Array.init 100000 (fun _ -> uint64 (rnd.Next()))
for a in 0 .. 10 do ignore (solve inp) // Measure this line

Я получаю около 150 мс для исходного кода и что-то между 50-75 мс с использованием новой версии.