以下代码(在位置内联注释)给出了我遇到的令人困惑的行为的最小示例。
本质上,为什么(2)导致糟糕的时空性能 while(1) 却没有?
以下代码在ghc版本8.4.3上编译并运行如下:ghc -prof -fprof-auto -rtsopts test.hs; ./test +RTS -p
{-# LANGUAGE Rank2Types #-}
import Debug.Trace
-- Not sure how to get rid of the record
data State = State {
-- (0) If vstate :: Float, the extra "hello"s go away
vstate :: forall a . (Fractional a) => a
}
step :: State -> State
step s =
-- (1) one "hello" per step
-- let vs = trace "hello" (vstate s) in
-- s { vstate = vs `seq` vstate s }
-- (2) increasing "hello"s per step
s { vstate = (trace "hello" (vstate s)) `seq` vstate s }
main :: IO ()
main = do
let initState = State { vstate = 0 }
-- (3) step 3 times
-- let res = step $ step $ step initState
-- print $ vstate res
-- (4) step 20 times to profile time/space performance
let res = iterate step initState
print $ vstate $ last $ take 20 res
print "done"
一个。用(1)和(3)注释,不使用
-O2
进行编译,该代码仅输出“hello” 3次,正如我期望的那样。b。注释掉(2)和(3),并在不使用
-O2
的情况下进行编译,该代码将输出“hello”八次。似乎每步输出一个额外的“hello”。 我不明白为什么会这样。 C。注释掉(1)和(4),并在不使用
-O2
的情况下进行编译,该代码的运行速度非常快。d。注释掉(2)和(4)并在不使用
-O2
的情况下进行编译,该代码运行非常缓慢,并且性能报告(如下所示)显示,与变体vstate
相比,对c
的调用更多,并且使用的内存更多。 我也不明白为什么会这样。 e。注释(2)和(4),并用
-O2
编译,该代码的行为与c
变体相同。因此,显然ghc可以优化变体d
中发生的任何病理行为。这是变体
c
的性能分析报告(快速): Mon Aug 13 15:48 2018 Time and Allocation Profiling Report (Final)
partial +RTS -p -RTS
total time = 0.00 secs (0 ticks @ 1000 us, 1 processor)
total alloc = 107,560 bytes (excludes profiling overheads)
COST CENTRE MODULE SRC %time %alloc
CAF GHC.IO.Handle.FD <entire-module> 0.0 32.3
CAF GHC.IO.Encoding <entire-module> 0.0 3.1
main Main partial.hs:(24,1)-(35,16) 0.0 13.4
main.res Main partial.hs:32:9-36 0.0 1.6
step Main partial.hs:(15,1)-(18,36) 0.0 1.1
step.vs Main partial.hs:17:9-37 0.0 46.1
individual inherited
COST CENTRE MODULE SRC no. entries %time %alloc %time %alloc
MAIN MAIN <built-in> 114 0 0.0 0.6 0.0 100.0
CAF Main <entire-module> 227 0 0.0 0.1 0.0 52.2
main Main partial.hs:(24,1)-(35,16) 228 1 0.0 2.7 0.0 52.1
vstate Main partial.hs:11:5-10 230 20 0.0 0.0 0.0 0.0
main.initState Main partial.hs:25:9-40 239 0 0.0 0.0 0.0 0.0
main.res Main partial.hs:32:9-36 234 0 0.0 0.0 0.0 0.0
step Main partial.hs:(15,1)-(18,36) 235 0 0.0 0.0 0.0 0.0
main.initState Main partial.hs:25:9-40 233 1 0.0 0.0 0.0 0.0
main.res Main partial.hs:32:9-36 231 1 0.0 1.6 0.0 49.4
step Main partial.hs:(15,1)-(18,36) 232 19 0.0 1.1 0.0 47.8
step.vs Main partial.hs:17:9-37 236 19 0.0 46.1 0.0 46.7
vstate Main partial.hs:11:5-10 237 190 0.0 0.0 0.0 0.6
main.initState Main partial.hs:25:9-40 238 0 0.0 0.6 0.0 0.6
CAF Debug.Trace <entire-module> 217 0 0.0 0.2 0.0 0.2
CAF GHC.Conc.Signal <entire-module> 206 0 0.0 0.6 0.0 0.6
CAF GHC.IO.Encoding <entire-module> 189 0 0.0 3.1 0.0 3.1
CAF GHC.IO.Encoding.Iconv <entire-module> 187 0 0.0 0.2 0.0 0.2
CAF GHC.IO.Handle.FD <entire-module> 178 0 0.0 32.3 0.0 32.3
CAF GHC.IO.Handle.Text <entire-module> 176 0 0.0 0.1 0.0 0.1
main Main partial.hs:(24,1)-(35,16) 229 0 0.0 10.7 0.0 10.7
这是变体
d
的性能分析报告(慢;没有-O2
): Mon Aug 13 15:25 2018 Time and Allocation Profiling Report (Final)
partial +RTS -p -RTS
total time = 1.48 secs (1480 ticks @ 1000 us, 1 processor)
total alloc = 1,384,174,472 bytes (excludes profiling overheads)
COST CENTRE MODULE SRC %time %alloc
step Main partial.hs:(15,1)-(21,60) 95.7 98.8
main.initState Main partial.hs:25:9-40 3.0 1.2
vstate Main partial.hs:11:5-10 1.4 0.0
individual inherited
COST CENTRE MODULE SRC no. entries %time %alloc %time %alloc
MAIN MAIN <built-in> 114 0 0.0 0.0 100.0 100.0
CAF Main <entire-module> 227 0 0.0 0.0 100.0 100.0
main Main partial.hs:(24,1)-(35,16) 228 1 0.0 0.0 100.0 100.0
vstate Main partial.hs:11:5-10 230 1048575 1.4 0.0 100.0 100.0
main.initState Main partial.hs:25:9-40 236 0 3.0 1.2 3.0 1.2
main.res Main partial.hs:32:9-36 234 0 0.0 0.0 95.7 98.8
step Main partial.hs:(15,1)-(21,60) 235 0 95.7 98.8 95.7 98.8
main.initState Main partial.hs:25:9-40 233 1 0.0 0.0 0.0 0.0
main.res Main partial.hs:32:9-36 231 1 0.0 0.0 0.0 0.0
step Main partial.hs:(15,1)-(21,60) 232 19 0.0 0.0 0.0 0.0
CAF Debug.Trace <entire-module> 217 0 0.0 0.0 0.0 0.0
CAF GHC.Conc.Signal <entire-module> 206 0 0.0 0.0 0.0 0.0
CAF GHC.IO.Encoding <entire-module> 189 0 0.0 0.0 0.0 0.0
CAF GHC.IO.Encoding.Iconv <entire-module> 187 0 0.0 0.0 0.0 0.0
CAF GHC.IO.Handle.FD <entire-module> 178 0 0.0 0.0 0.0 0.0
CAF GHC.IO.Handle.Text <entire-module> 176 0 0.0 0.0 0.0 0.0
main Main partial.hs:(24,1)-(35,16) 229 0 0.0 0.0 0.0 0.0
以下是一些有关为什么发生这种情况的注释/猜测/问题:
vstate
单一化为vstate :: Float
时,病理行为消失了。但是我不明白为什么缺少在位置(2)中的let绑定(bind)会导致如此糟糕的时间/空间性能。 realToFrac
对输入的浮点型数字进行“固定化”来解决(如果有人好奇,则提交为here)。我知道用-O2
进行编译可以解决此最小示例中的问题,但是我在较大的代码库中尝试了此操作,但无法解决性能问题。 (我们在较大的代码库中需要rank-2多态性的原因是,将ad
库用于autodiff。)是否有比realToFrac
更有原则的修复,例如可以应用的内联专门化? 最佳答案
forall a . (Fractional a) => a
是一种函数类型。
它具有两个参数,一个类型为(a :: *)
和一个类型为Fractional a
的实例。每当您看到=>
时,它在操作上就是一个函数,并且会以GHC的核心表示形式编译为一个函数,有时还会在机器代码中保留为一个函数。 ->
和=>
之间的主要区别在于,后者的参数不能由程序员明确给出,并且它们始终由实例解析隐式填充。
首先让我们看一下快速的step
:
step :: State -> State
step (State f) =
let vs = trace "hello" f
in State (vs `seq` f)
在这里,
vs
具有不确定的Fractional
类型,默认为Double
。如果您打开-Wtype-defaults
警告,GHC会向您指出。从vs :: Double
开始,它只是一个数值,由返回的闭包捕获。正确,vs `seq` f
是一个函数,因为它具有函数类型forall a . (Fractional a) => a
,并且由GHC替换为实际的lambda表达式。此lambda抽象化两个参数,将vs
捕获为自由变量,然后将这两个参数传递给f
。因此,每个
step
都会创建一个新的函数闭合,该函数将捕获vs :: Double
。如果我们三次调用step
,我们将得到三个带有三个Double
的闭包,每个闭包均引用前一个闭包。然后,当我们编写vstate (step $ step $ step initState)
时,我们再次默认为Double
,GHC会使用Fractional Double
实例调用此闭包。所有vs
-es都使用Fractional Double
调用先前的闭包,但是每个vs
仅被评估一次,因为它们是常规的惰性Double
值,不会重新计算。但是,如果启用
NoMonomorphismRestriction
,则vs
通用化为forall a. Fractional a => a
,因此它也成为一个函数,并且其调用也不再被记忆。因此,在这种情况下,快速版本的行为与慢速版本相同。现在,慢速
step
:step :: State -> State
step (State f) = State ((trace "hello" f) `seq` f)
这具有步骤数中的指数调用数,因为
step f
两次调用f
,并且没有优化就不会共享计算,因为这两个调用都在lambda下进行。在(trace "hello" f) `seq` f
中,对f
的第一个调用默认为Fractional Double
,而第二个调用与前面一样只传递了隐式Fractional a
实例。如果我们打开优化,GHC会观察到第一个
f
调用不依赖于函数参数,并且将trace "hello" f
float 到一个let绑定(bind)中,从而产生了与快速版本中几乎相同的代码。