以下代码(在位置内联注释)给出了我遇到的令人困惑的行为的最小示例。

本质上,为什么(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

以下是一些有关为什么发生这种情况的注释/猜测/问题:
  • 降级性能与增加的“hello”计数之间有什么关系?病理版本似乎每增加一个步骤就会输出一个“hello”。为什么?
  • 我知道Haskell中的多态性很慢,如this StackOverflow question中所述。这可能是问题的一部分,因为当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)中,从而产生了与快速版本中几乎相同的代码。

    07-27 18:36