[GHC] #10711: Defining mapM_ in terms of traverse_ causes substantial blow-up in ByteCodeAsm

GHC ghc-devs at haskell.org
Fri Jul 31 08:30:15 UTC 2015


#10711: Defining mapM_ in terms of traverse_ causes substantial blow-up in
ByteCodeAsm
-------------------------------------+-------------------------------------
        Reporter:  bgamari           |                   Owner:  bgamari
            Type:  bug               |                  Status:  new
        Priority:  normal            |               Milestone:
       Component:  Compiler          |                 Version:
      Resolution:                    |                Keywords:
Operating System:  Unknown/Multiple  |            Architecture:
 Type of failure:  Compile-time      |  Unknown/Multiple
  performance bug                    |               Test Case:  ghcirun004
      Blocked By:                    |                Blocking:
 Related Tickets:                    |  Differential Revisions:
-------------------------------------+-------------------------------------

Comment (by nomeata):

 > Experimentally bgamari's test program does ~n2 allocations and takes ~n3
 total time in the Applicative version, while the Monad version runs in
 linear allocations and time.

 That and the fact that in comment:4 I need the associativity law, sounds
 like there is a quadratic behavior due to wrongly associated binds.

 Let’s see if I can evaluate my way by hand through it.

 {{{#!hs
 -- These fragments from bgamari’s test case
 let t n = Thing n cr2
 let cr2 = const $ return 2

    run (t 1 >> (t 2 >> t 3))
 == run (Thing 1 (cr2 >=> (\_ -> (t 2 >> t 3))))
 == run ((cr2 1 >=> (\_ -> (t 2 >> t 3))) 1)
 == run (cr2 1 >>=  (\_ -> (t 2 >> t 3)))
 == run (return 2 >>= (\_ -> (t 2 >> t 3)))
 == run ((\_ -> (t 2 >> t 3)) 2)
 == run (t 2 >> t3)
 == run (Thing 2 (cr2 >=> (\_ -> t 3)))
 == run ((cr2 2 >=> (\_ -> t 3)) 1)
 == run (cr2 2 >>=  (\_ -> t 3))
 == run (return 2 >>= (\_ -> t 3))
 == run ((\_ -> t 3) 2)
 == run (t 3)
 == run (Thing 3 cr2)
 == run (cr2 3)
 == run (return 2)
 == 2
 }}}

 For the applicative version, based on the empirical implementation, I
 assume that some parts of the code are kept alive for too long, and
 possibly be traversed multiple times. So here we go:

 {{{#!hs
 let cri = \_ -> return id)
 let ri  = (\x -> return (id x))

    run (t 1 *> (t 2 *> t 3))
 == run ((t 1 >>= cri) >>= (\x2 -> (t 2 *> t 3) >>= (\x3 -> return (x2
 x3))))
 == run ((Thing 1 (cr2 >=> cri)) >>= (\x2 -> (t 2 *> t 3) >>= (\x3 ->
 return (x2 x3))))
 == run (Thing 1 ((cr2 >=> cri) >=> (\x2 -> (t 2 *> t 3) >>= (\x3 -> return
 (x2 x3)))))
 == run (((cr2 >=> cri) >=> (\x2 -> (t 2 *> t 3) >>= (\x3 -> return (x2
 x3)))) 1)
 == run (((cr2 >=> cri) >=> (\x2 -> (t 2 *> t 3) >>= (\x3 -> return (x2
 x3)))) 1)
 == run (((cr2 >=> cri) 1 >>= (\x2 -> (t 2 *> t 3) >>= (\x3 -> return (x2
 x3)))))
 == run (((cr2 1 >>= cri) >>= (\x2 -> (t 2 *> t 3) >>= (\x3 -> return (x2
 x3)))))
 == run (((return 2 >>= cri) >>= (\x2 -> (t 2 *> t 3) >>= (\x3 -> return
 (x2 x3)))))
 == run (((cri 2) >>= (\x2 -> (t 2 *> t 3) >>= (\x3 -> return (x2 x3)))))
 == run (((return id) >>= (\x2 -> (t 2 *> t 3) >>= (\x3 -> return (x2
 x3)))))
 == run ((\x2 -> (t 2 *> t 3) >>= (\x3 -> return (x2 x3))) id)
 == run ((t 2 *> t 3) >>= ri) -- *¹
 == run (((t 2 >>= cri) >>= (\x2 -> t3) >>= (\x3 -> return (x2 x3)))) >>=
 ri)
 == ... -- as before
 == run ((t 3 >>= ri) >>= ri)
 == run (Thing 3 (cr2 >=> ri) >>= ri)
 == run (Thing 3 ((cr2 >=> ri) >=> ri)) -- *²
 == run (((cr2 >=> ri) >=> ri) 3)
 == run ((cr2 >=> ri) 3 >>= ri)
 == run ((cr2 3 >>= ri) >>= ri)
 == run ((return 2 >>= ri) >>= ri)
 == run (ri 2 >>= ri)
 == run (return 2 >>= ri)
 == run (ri 2)
 == run (return 2)
 == 2
 }}}

 `*¹`: I think this is interesting. Whereas above, `run (a >> b)` will
 eventually reduce to `run b`, here we get `run (b >>= complex_return)`,
 with one `complex_return` for every element in the list. This explains at
 least some of the blow up: We build this chain, and then we have to
 eventually tear it down again.

 `*²` And again we traverse this whole chain of pointless `ri`’s.

 Hmm, not sure if and how this exposition explains the quadratic blow up in
 allocations, though. Do we traverse the stack of `>=> ri` once per element
 somehow, similar to a wrongly associated `++`? But I don’t see it right
 now.

--
Ticket URL: <http://ghc.haskell.org/trac/ghc/ticket/10711#comment:7>
GHC <http://www.haskell.org/ghc/>
The Glasgow Haskell Compiler


More information about the ghc-tickets mailing list