Specialization is an optimization technique used by GHC to eliminate the
performance overhead of ad-hoc polymorphism and enable other powerful
optimizations. However, specialization is not free, since it requires more work
by GHC during compilation and leads to larger executables. In fact, excessive
specialization can result in significant increases in compilation cost and
executable size with minimal runtime performance benefits. For this reason, GHC
pessimistically avoids excessive specialization by default and may leave
relatively low-cost performance improvements undiscovered in doing so.
Optimistic Haskell programmers hoping to take advantage of these missed
opportunities are thus faced with the difficult task of discovering and enacting
an optimal set of specializations for their program while balancing any
performance improvements with the increased compilation costs and executable
sizes. Until now, this dance was a clunky one involving desperately wading
through GHC Core dumps only to come up with a precarious, inefficient,
unmotivated set of pragmas and/or GHC flags that seem to improve performance.
In this two-part series of posts, I describe the recent work we have done to
improve this situation and make optimal specialization of Haskell programs more
of a science and less of a dark art. In this first post, I will
- give a comprehensive introduction to GHC’s specialization optimization,
- explore the various facilities that GHC provides for observing and controlling it, and
- present a simple framework for thinking about the trade-offs of
specialization.
In the next post of the series, I will
- present the new tools and techniques we have developed to diagnose
performance issues resulting from ad-hoc polymorphism,
- demonstrate how these new tools can be used to systematically
identify useful specializations, and
- make sense of their impact in terms of the
framework described in this post.
The intended audience of this post includes intermediate Haskell developers who
want to know more about specialization and ad-hoc polymorphism in GHC, and
advanced Haskell developers who are interested in systematic approaches to
specializing their applications in ways that minimize compilation cost and
executable sizes while maximizing performance gains.
This work was made possible thanks to Hasura, who have
supported many of Well-Typed’s successful initiatives to
improve tooling for commercial Haskell users.
I presented a summary of the content in this post on The Haskell Unfolder:
Overloaded functions are common in Haskell, but they come with a cost. Thankfully, the GHC specialiser is extremely good at removing that cost. We can therefore write high-level, polymorphic programs and be confident that GHC will compile them into very efficient, monomorphised code. In this episode, we’ll demystify the seemingly magical things that GHC is doing to achieve this.