πŸš€ UllrichLumina

Reading GHC Core

Reading GHC Core

πŸ“… | πŸ“‚ Category: Programming

For many Haskell developers, GHC Core remains a mysterious, often intimidating, black box. Yet, understanding this intermediate representation is not just an academic exercise; it’s a powerful skill that unlocks profound insights into your code’s performance and behavior. GHC Core is the language into which the Glasgow Haskell Compiler translates your high-level Haskell code, before further optimization and machine code generation. By learning to decipher this low-level form, you gain the ability to pinpoint performance bottlenecks, verify compiler optimizations, and truly comprehend how Haskell’s unique features, like laziness and type classes, are implemented under the hood. It’s a bridge between the elegant abstraction of Haskell and the concrete reality of execution.

Why Understanding GHC Core Matters for Haskell Performance

Diving into GHC Core is akin to looking under the hood of a finely tuned engine. While Haskell abstracts away many low-level details, performance-critical applications often demand a deeper understanding. GHC Core provides a precise view of what the compiler has done with your code, revealing the exact transformations applied during various optimization passes. This visibility is crucial when optimizing Haskell performance, allowing developers to confirm if a specific optimization strategy has had the desired effect, or if unexpected re-computations or memory allocations are occurring.

One primary benefit is the ability to diagnose performance issues that are not immediately obvious from the source code. For instance, you might expect a function to be heavily optimized, but upon inspecting its Core, you discover it’s not being inlined as aggressively as anticipated, or that an unexpected thunk is being created. This insight allows for targeted adjustments to your source code or compiler flags. As Simon Peyton Jones, a key figure in GHC’s development, once noted, “Core is the language of the compiler’s internal reasoning, and understanding it helps you reason along with the compiler.” This perspective empowers developers to write more efficient and predictable Haskell programs, moving beyond guesswork to informed optimization.

Furthermore, understanding GHC Core demystifies many advanced Haskell concepts. How does strictness analysis work? How are type classes dictionary-passed? What does the worker/wrapper transformation actually achieve? All these questions are answered concretely within the structure of GHC Core. This knowledge not only helps in debugging but also improves your overall understanding of the GHC compiler and its capabilities, leading to more robust and performant codebases. It provides a foundational understanding of the STG machine, the abstract machine GHC Core targets, which directly impacts how lazy evaluation unfolds at runtime.

Tools and Techniques for Inspecting GHC Core

Accessing and reading GHC Core doesn’t require arcane knowledge; GHC itself provides the necessary tools. The primary mechanism is via specific compiler flags that instruct GHC to output its intermediate representations. Once generated, these files can be viewed with standard text editors, though specialized tools can enhance the experience. The process is straightforward and should become a regular part of your performance debugging toolkit.

  1. Generate Core Files: Compile your Haskell module with the -ddump-simpl flag (or -ddump-ds, -ddump-rn, etc., for earlier stages). For a more readable output, add -dppr-debug, -dppr-user-length 100, and -dsuppress-uniques. For example: ghc -O2 MyModule.hs -ddump-simpl -dppr-debug -dsuppress-uniques > MyModule.core.
  2. Examine the Output: The generated .core file contains the simplified GHC Core. Look for the top-level bindings corresponding to your functions. Pay attention to changes in variable names (often prefixed with $s or $w after optimizations like strictness analysis), and the structure of applications.
  3. Use Core Lint: GHC also provides a linting tool (-fplugin GHC.Core.Lint) that can check the generated Core for consistency and correctness. While not directly for “reading,” it helps ensure the Core itself is valid, which can be useful when experimenting with compiler plugins or extensions.
  4. Visualizer Tools: For larger projects, raw text files can be overwhelming. Tools like GHC’s internal Core pretty-printer or experimental community projects aim to provide more navigable views, though text parsing remains the most common method.

When you dump GHC Core, you are essentially looking at the compiler’s last major intermediate representation before code generation. It’s a typed lambda calculus, extended with constructs like case expressions for pattern matching and let/letrec for local bindings. Understanding these basic building blocks is paramount to effectively reading GHC Core and interpreting the transformations applied by the optimization passes. This is where the compiler applies sophisticated rewrite rules to improve efficiency.

Key Concepts in Deciphering GHC Core

To effectively read GHC Core, you need to familiarize yourself with its fundamental elements. GHC Core is a relatively small language, but its expressiveness allows it to represent all Haskell constructs. It’s a typed lambda calculus, meaning every expression has an associated type, and functions are explicitly represented as lambda abstractions.

The GHC Core language is a simplified, explicit version of Haskell, primarily consisting of:

  • Variables: Represented as identifiers, often with type annotations and unique suffixes (e.g., x_a1).
  • Literals: Basic values like numbers, characters, and strings.
  • Lambda Abstractions (\): Functions are represented as \ arg -> body. All functions are explicitly curried.
  • Applications: Function application f x.
  • Let/Letrec Bindings: let x = rhs in body for non-recursive bindings, and letrec x = rhs in body for recursive ones. These define local variables.
  • Case Expressions: case expr of { pat1 -> res1; pat2 -> res2; ... }. This is the primary mechanism for pattern matching, handling algebraic data types, and also for forcing evaluation of thunks.
  • Type Annotations: Explicit type information is pervasive, helping you understand the exact types being manipulated, including those inferred by the type system.
Write Haskell as fast as C: exploiting strictness, laziness and recursion 2. [Haskell as fast as C: working at a high altitude for low level performance](http://donsbot.wordpress.com/2008/06/04/haskell-as-fast-as-c-working-at-a-high-altitude-for-low-level-performance/) 3. [RWH: Chapter 25. Profiling and optimization](http://book.realworldhaskell.org/read/profiling-and-optimization.html) 4. [High-Performance Haskell talk at CUFP](http://blog.johantibell.com/2010/09/slides-from-my-high-performance-haskell.html) (slide 65-80)

GHC Core is the System FC language into which all Haskell is translated. The (approximate) grammar for Core is given by:

enter image description here

Core is closely related to the simpler and better known System F. All transformations GHC does on the Core level are type-preserving refactorings of this Core representation, to improve performance. And, not so well known, you can write directly in Core to program GHC.

GHC Core fits in the compiler pipeline (as it was in 2002, sans-LLVM and CMM):

enter image description here

The primary documents to learn about GHC Core are:

Related material that can aid understanding:

Core in turn is translated into STG code, which looks something like:

enter image description here

The funny names in Core are encoded in the “Z-encoding”:

enter image description here

GHC Core’s types and kinds (from Tolmach’s paper):

enter image description here

Finally, GHC’s primops appear regularly in GHC Core output, when you have optimized your Haskell down to the basic instructions GHC knows about. The primop set is given as a set of Core functions in a pre-processed file.