web-dev-qa-db-ja.com

このプログラムのF#バージョンがHaskellの6倍速いのはなぜですか?

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)

上記の2つのプログラムは ストック最大化問題 のソリューションであり、時間はRun Codeボタンの最初のテストケースのものです。

何らかの理由でF#バージョンは約6倍高速ですが、遅いライブラリ関数を命令型ループに置き換えた場合、少なくとも3倍、おそらく10倍高速化できると確信しています。

Haskellバージョンも同様に改善できますか?

私は学習目的で上記を行っており、一般的に、効率的なHaskellコードの書き方を見つけるのは難しいと感じています。

51
Marko Grdinic

ByteString に切り替えて、(ベクトルではなく)単純なHaskellリストを使用すると、より効率的なソリューションが得られます。 単一の左折りで解決関数を書き換え、Zipと右スキャンをバイパスすることもできます (1)。全体的に、私のマシンでは、Haskellソリューションと比較してパフォーマンスが20倍向上しています (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を実装するこの回答の以前のバージョンについては、 edits を参照してください。
2. HackerRank Webサイトでは、さらに大きなパフォーマンスの改善が示されています。

75
behzad.nouri

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レイヤー(ユニコード解析)および文字列分割によって制限されます。これは、バイトバッファーに読み取り、レクサーを手動で書き込むことによって最適化できます。

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
54
Jon Harrop

記録のためだけに、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

新しいバージョンを使用すると、元のコードで約150ミリ秒、50〜75ミリ秒で取得できます。

45
Tomas Petricek