[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