[GHC] #14610: newtype wrapping of a monadic stack kills performance

GHC ghc-devs at haskell.org
Sun Dec 24 12:23:33 UTC 2017


#14610: newtype wrapping of a monadic stack kills performance
-------------------------------------+-------------------------------------
           Reporter:  mrkkrp         |             Owner:  (none)
               Type:  bug            |            Status:  new
           Priority:  normal         |         Milestone:
          Component:  Compiler       |           Version:  8.2.2
           Keywords:                 |  Operating System:  Unknown/Multiple
       Architecture:                 |   Type of failure:  None/Unknown
  Unknown/Multiple                   |
          Test Case:                 |        Blocked By:
           Blocking:                 |   Related Tickets:
Differential Rev(s):                 |         Wiki Page:
-------------------------------------+-------------------------------------
 I have a project where I in one module (A) I decided to build something
 like
 a minimal framework for the other bigger module (B) to use. One of the
 main
 points of the framework is that the monadic stacks are hidden behind
 `newtype`s and in those monads you can only use the functions that the
 module A provides.

 The module A can be found here:

 https://github.com/mrkkrp/mmark/blob/master/Text/MMark/Parser/Internal.hs

 There are two monadic stack wrapped with newtypes: `BParser` and
 `IParser`.

 The module B is this:

 https://github.com/mrkkrp/mmark/blob/master/Text/MMark/Parser.hs

 But it's not really relevant.

 Now if I change `newtype`s to `type` synonyms like so:

 {{{
 type BParser a = ParsecT MMarkErr Text (State BlockState) a
 type IParser a = StateT InlineState (Parsec MMarkErr Text) a
 }}}

 and do corresponding minor corrections, I get 2x less allocations and
 almost 2x faster code (before):

 {{{
 Case                                           Allocated  GCs     Max
 with file: data/bench-yaml-block.md              119,080    0  11,088
 with file: data/bench-thematic-break.md           74,368    0  10,224
 with file: data/bench-heading.md                 901,928    0   9,432
 with file: data/bench-fenced-code-block.md       145,744    0   9,368
 with file: data/bench-indented-code-block.md     124,312    0   9,368
 with file: data/bench-unordered-list.md        2,010,496    1  10,784
 with file: data/bench-ordered-list.md          2,025,016    1  10,728
 with file: data/bench-blockquote.md            1,961,672    1  42,648
 with file: data/bench-paragraph.md             2,084,104    1  42,648
 with file: data/comprehensive.md              25,899,496   24  44,200

 benchmarking with file: data/comprehensive.md
 time                 3.590 ms   (3.578 ms .. 3.601 ms)
                      1.000 R²   (1.000 R² .. 1.000 R²)
 mean                 3.555 ms   (3.546 ms .. 3.565 ms)
 std dev              31.07 μs   (24.63 μs .. 39.90 μs)
 }}}

 After:

 {{{
 Case                                           Allocated  GCs     Max
 with file: data/bench-yaml-block.md              116,864    0  11,088
 with file: data/bench-thematic-break.md           64,776    0  10,392
 with file: data/bench-heading.md                 615,672    0   9,432
 with file: data/bench-fenced-code-block.md       144,736    0   9,368
 with file: data/bench-indented-code-block.md     123,352    0   9,368
 with file: data/bench-unordered-list.md          795,072    0  41,568
 with file: data/bench-ordered-list.md            808,808    0  41,512
 with file: data/bench-blockquote.md              826,392    0  41,568
 with file: data/bench-paragraph.md               881,432    0  41,568
 with file: data/comprehensive.md              10,945,104   10  44,440

 benchmarking with file: data/comprehensive.md
 time                 2.451 ms   (2.448 ms .. 2.456 ms)
                      1.000 R²   (1.000 R² .. 1.000 R²)
 mean                 2.432 ms   (2.427 ms .. 2.437 ms)
 std dev              15.86 μs   (13.16 μs .. 19.01 μs)
 }}}

 I'm not great at reading non-trivial core, but I gave it a shot and dumped
 some core. One thing I noticed that core for the module A is the same in
 both cases. Then the problem is an inter-module problem, probably like
 when
 you don't dump definitions into interface files with `INLINEABLE` and end
 up
 without specialization, but here it perhaps has to do with the fact that B
 has no information about internals of the monadic stacks from A, but I'm
 not
 sure.

 Here is the beginnings of core dumps (before):

 {{{
 ==================== Tidy Core ====================
 2017-12-23 04:55:17.967944505 UTC

 Result size of Tidy Core
   = {terms: 210,169,
      types: 209,675,
      coercions: 82,492,
      joins: 973/3,753}
 }}}

 After:

 {{{
 ==================== Tidy Core ====================
 2017-12-23 04:58:46.386265108 UTC

 Result size of Tidy Core
   = {terms: 301,256,
      types: 263,104,
      coercions: 28,560,
      joins: 1,726/5,393}
 }}}

 So it looks like `newtype` wrapping reduces GHC's ability to create join
 points SPJ talked about recently.

 I do understand that you expect a minimal reproducing example but I could
 not produce it even though I spent several hours in futile attempts. I
 started by creating a small project, defining a similar stack with a
 `newtype` wrapper and using it in a simple parser in another module. Then
 I
 benchmarked the parser. There is no difference between `newtype`d and code
 and the code that uses just type synonyms. I tried different variations,
 no difference.

 The core output is too big for me to analyze, it's like 25 Mb and 33 Mb,
 and I have no idea what's going on there.

 You're welcome to experiment with the repo, there are benchmarks for
 memory usage and criterion benchmarks for speed of execution:

 https://github.com/mrkkrp/mmark

 If you like I can create a separate branch with newtypes replaced with
 type synonyms, so you can compare the both approaches easier.

 I'm submitting this because my friend convinced me that it's better to let
 you know (even though I could not create a minimal reproducing example on
 my own) than to let it go completely unnoticed.

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


More information about the ghc-tickets mailing list