2016-01-16 5 views
9

Ich habe den folgenden Code, der abgespeckt wurde und ich denke, so minimal wie möglich, dass einige sehr seltsame Verhalten hat.Unerwartetes Speicherwachstum mit Control.Monad foldM

Der Code besteht aus zwei Quelldateien: man einige Daten zu definieren:

module MyFunction where 

data MyFunction = 
    MyFunction { 
     functionNumber :: Int, 
     functionResult :: IO String 
     } 

makeMyFunction :: Show a => Int -> IO a -> MyFunction 
makeMyFunction number result = MyFunction { 
    functionNumber = number, 
    functionResult = result >>= return . show } 

Und das andere ist Main:

module Main (main) where 

import System.CPUTime (getCPUTime) 
import Data.List (foldl') 
import Data.Foldable (foldlM) 
import Control.Monad (foldM) 
import MyFunction 

exampleFunction = do 
    --let x = foldl' (\a b -> a `seq` (a + b)) 0 [1..20000000]  -- This works 
    --x <- foldlM (\a b -> a `seq` return (a + b)) 0 [1..20000000] -- This works (*) 
    x <- foldM (\a b -> a `seq` return (a + b)) 0 [1..20000000] -- This doesn't 
    print x 
    return() 

runFunction fn = do 
    result <- functionResult fn 
    duration <- getCPUTime 
    if result /= "()" 
     then putStrLn "" 
     else return() 
    putStrLn (show (fromIntegral duration/(10^9)) ++ "ms") 
    return fn 

main = do 
    runFunction (makeMyFunction 123 exampleFunction) 
    return() 

Der obige Code wie (zusammengestellt 7.10.3 mit GHC mit Stack 1.0.0 mit Standard-Flags) hat einen schnellen Anstieg der Speicherauslastung (über 1 GB) und dauert in der Regel 3,3 Sekunden.

Wenn ich eine Änderung an den Code, zum Beispiel:

  • Verwenden Sie eine der kommentierten Alternativen zum Problem Linie
  • aus jeder Zeile herausnehmen runFunction

Die Speichernutzung wird minimal bleiben und dauert nur etwa 1 Sekunde.

Ein Merkmal, das ich denke, ist sehr überraschend für mich ist, dass foldM mit foldlM ersetzen (was soweit ich foldM = foldlM wissen) das Problem behebt.

Auch Änderungen an Code, die ich nicht sehe, hat eine Beziehung zu den Problemzeilen des Codes auch das Problem behebt. Zum Beispiel Entfernen der letzten putStrLn.

Eine weitere Kuriosität ist, dass, wenn ich das MyFunction Modul in das Hauptmodul fusionieren, während es das Problem nicht beheben, es foldlM tatsächlich verursacht als foldM mit übermäßigen Speichern verhalten.

In dem realen Code, der dies herkam, habe ich eine große Anzahl exampleFunction s, und es ist deutlich mehr Main Code, und jedes begegnet mir so oft diese Art von unerklärlicher Speichernutzung von Funktionen, die in der Regel gelöst werden können durch eine Art Voodoo.

Ich bin auf der Suche nach einer Erklärung für das Verhalten. Wenn ich weiß, warum das passiert, kann ich dann versuchen, es zu vermeiden. Könnte das ein Compiler-Problem oder vielleicht nur ein Missverständnis sein?

(*) Ich habe das sekundäre Problem hervorgehoben, das das gleiche Speicherwachstum mit FoldlM verursacht.

+4

Die Tatsache, dass sich Dinge zwischen den Modulen auf das Verhalten * stark * auswirken, deutet darauf hin, dass GHCs Inliner beteiligt ist. Eine Transformation, die durch Inlining ermöglicht wird, ist entweder * Ihnen * oder * Ihnen * schaden. – dfeuer

+0

ein nicht verwandtes Problem ist Ihre Arithmetik in exampleFunction wird standardmäßig auf Integer, die ziemlich langsam sein wird. Schalten Sie "-Wall" ein. – jberryman

+0

@jberryman exampleFunction soll nur ein Beispiel sein - um den Speicherzuwachs zu zeigen. – pticawr

Antwort

3

Hier foldlM von Foldable.hs (GHC)

-- | Monadic fold over the elements of a structure, 
-- associating to the left, i.e. from left to right. 
foldlM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b 
foldlM f z0 xs = foldr f' return xs z0 
    where f' x k z = f z x >>= k 

und foldM von Monad.hs

foldM   :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b 
{-# INLINEABLE foldM #-} 
{-# SPECIALISE foldM :: (a -> b -> IO a) -> a -> [b] -> IO a #-} 
{-# SPECIALISE foldM :: (a -> b -> Maybe a) -> a -> [b] -> Maybe a #-} 
foldM   = foldlM 

I diese Definitionen in einen separaten Modultest eingebracht und getestet, um die Ausführung mit und ohne INLINEABLE/SPESIALISE Linien. Was auch immer der Grund ist, die SPECIALIZE-Direktiven weggelassen zu haben, und die Ausführungszeit und Speicherauslastung waren wie bei fulllM.

Nach einem wenig mehr zu graben, zu entfernen Linie

{-# SPECIALISE foldM :: (a -> b -> IO a) -> a -> [b] -> IO a #-} 

bewirkt am meisten.

+3

Der Grund ist, dass Dinge, die mit 'SPECIALISE' gekennzeichnet sind, auf den gegebenen Typ spezialisiert sind, und wenn dieser Typ bei Verwendungssites gesehen wird, wird die spezialisierte Version verwendet; GHC (allgemein?) Berücksichtigt * nicht * stattdessen Inlining. Für etwas so Einfaches wie 'foldM' scheint Spezialisierung fast immer schlimmer zu sein als Inlining im Allgemeinen, daher sollten diese Pragmas entfernt werden. – dfeuer

+0

@dfeuer; J.J. : Beim Entfernen der Spezialisierung wird ein Problem gelöst (bei Verwendung der Quelldateien wie oben). Können Sie erklären, warum das Problem beim Zusammenführen der Quelldateien in eins auftritt, aber sowohl für foldM als auch für foldlM, das die Speicherauslastung anzeigt? – pticawr

+0

@pticawr ohne Optimierung Flag wie '-O1'? –