为什么这个程序的F#版本比Haskell快6倍?

时间:2016-05-30 13:20:24

标签: performance haskell f#

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.17s):

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)

以上两个程序是Stock Maximize problem的解决方案,时间是Run Code按钮的第一个测试用例。

出于某种原因,F#版本大约快6倍,但我很确定如果我用强制循环替换慢库函数,我可以将其加速至少3倍,更可能是10倍。

Haskell版本可以同样改进吗?

我正在进行上述操作以进行学习,总的来说,我发现很难弄清楚如何编写有效的Haskell代码。

3 个答案:

答案 0 :(得分:75)

如果切换到ByteString并坚持使用普通的Haskell列表(而不是向量),您将获得更有效的解决方案。您也可以使用单个左侧折叠重写求解函数,然后绕过zip并右扫描(1)。 总的来说,在我的机器上,与 Haskell 解决方案(2)相比,我获得了20倍的性能提升。

在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。有关使用solvezip实现scanr的此答案的早期版本,请参阅edits
<子> 2。 HackerRank网站甚至显示出更大的性能提升。

答案 1 :(得分:54)

如果我想在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

根据我的测量结果,这比你的F#快11倍。

然后性能受到IO层(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

答案 2 :(得分:45)

仅供记录,F#版本也不是最佳选择。我认为此时并不重要,但如果人们想要比较性能,那么值得注意的是它可以更快。

我没有非常努力(你可以通过使用限制性突变来加快速度,这不会违反F#的性质),但使用Seq而不是Array进行简单的更改在正确的位置(以避免分配临时数组)使代码快2到3倍:

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

原始代码大约需要150ms,使用新版本大约需要50-75ms。