web-dev-qa-db-fra.com

Pourquoi la version F # de ce programme est-elle 6 fois plus rapide que celle de Haskell?

Version 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

Version 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)

Les deux programmes ci-dessus sont les solutions pour le Stock Maximize problem et les heures sont pour le premier cas de test du Run Code bouton.

Pour une raison quelconque, la version F # est environ 6 fois plus rapide, mais je suis sûr que si je remplaçais les fonctions de bibliothèque lentes par des boucles impératives, je pourrais l'accélérer d'au moins 3 fois et plus probablement 10 fois.

La version Haskell pourrait-elle être améliorée de la même manière?

Je fais ce qui précède à des fins d'apprentissage et en général, je trouve difficile de comprendre comment écrire du code Haskell efficace.

51
Marko Grdinic

Si vous passez à ByteString et que vous vous en tenez aux listes Haskell simples (au lieu des vecteurs), vous obtiendrez une solution plus efficace. Vous pouvez également réécrire la fonction de résolution avec un seul pli gauche et contourner Zip et scan à droite (1). Dans l'ensemble, sur ma machine, j'obtiens une amélioration des performances de 20 fois par rapport à votre solution Haskell  (2).

Le code Haskell ci-dessous est plus rapide que le code 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. Voir modifications pour une version antérieure de cette réponse qui implémente solve en utilisant Zip et scanr.
2. Le site Web HackerRank montre une amélioration des performances encore plus importante.

75
behzad.nouri

Si je voulais le faire rapidement en F #, j'éviterais toutes les fonctions d'ordre supérieur à l'intérieur de solve et j'écrirais simplement une boucle impérative de style 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

Selon mes mesures, c'est 11 fois plus rapide que votre F #.

Ensuite, les performances sont limitées par la couche IO (analyse unicode) et le fractionnement de chaînes. Ceci peut être optimisé en lisant dans un tampon d'octets et en écrivant le lexer à la main:

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

Juste pour mémoire, la version F # n'est pas non plus optimale. Je ne pense pas que cela compte vraiment à ce stade, mais si les gens voulaient comparer les performances, il convient de noter que cela peut être fait plus rapidement.

Je n'ai pas essayé très fort (vous pouvez certainement le rendre encore plus rapide en utilisant une mutation restreinte, ce qui ne serait pas contraire à la nature de F #), mais un simple changement pour utiliser Seq au lieu de Array dans les bons endroits (pour éviter d'allouer des tableaux temporaires) rendent le code environ 2x à 3x plus rapide:

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'

Si tu utilises Seq.Zip, vous pouvez également supprimer l'appel take (car Seq.Zip tronque automatiquement la séquence). Mesuré à l'aide de #time en utilisant l'extrait de code suivant:

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

J'obtiens environ 150 ms pour le code d'origine et quelque chose entre 50 et 75 ms en utilisant la nouvelle version.

45
Tomas Petricek