In this post, I will explain a new feature which allows GHC to store the Core definitions of entire modules in interface files. The motivation is severalfold: faster GHCi start-times, faster Template Haskell evaluation times and the possibility of using the definitions for other program analysis tasks.

This feature is implemented in MR !7502 and will appear in GHC 9.6. The work was funded by Mercury who benefited from a 40% improvement to compilation times on their Template Haskell heavy code base.

Motivation

The point of this work was to be able to restart the compiler pipeline at the point just after optimisation and before code generation. (See the GHC chapter in the AOSA book for background on the compiler pipeline.) In particular, we wanted to be able to generate bytecode on demand for any module, as this can significantly speed up start times for projects in GHCi.

The compiler writes interface files (.hi files) to pass on information about a module that subsequent modules will need to know. Interface files contain information about what functions are defined by a module, their types, what a module exports and so on. They may contain different amounts of detail: for example, if you compile with optimisations turned on, the interface file will contain more information about certain bindings, such as their demand signatures or unfoldings. For more information about interface files, you can consult the wiki.

Adding Core definitions to interface files makes it possible to defer the choice of backend (between no code, static object code, dynamic object code, bytecode) until more is known about the necessary targets. In particular:

  • GHCi can quickly convert previously-generated Core definitions to bytecode when loading a module, rather than needing to run the full compiler pipeline to generate bytecode for many modules even if they have previously been compiled.

  • Cabal pessimises build times by building both static and dynamic objects under the assumption that you will eventually need dynamic objects to run Template Haskell splices. With the Core definitions at hand, we can delay this choice until we know for sure we need to do the work. Moreover, rather than compiling and linking object code we can interpret bytecode, which is typically a faster operation for TH splices.

  • The Core program can also be useful for whole program analysis tasks. For example, the external STG interpreter could read these interface files and convert the result into its own STG format for running on the STG interpreter.

Core Definitions in Interface Files

In GHC 9.6, the interface file format is extended with a new field which contains complete Core bindings for the modules. A new command-line flag is available to enable this:

-fwrite-if-simplified-core

Write the Core definitions of the functions defined in this module to the interface file after simplification.

If you compile a module with -fwrite-if-simplified-core then you will see a new section called “extra-decls” when you dump the contents of an interface file with --show-iface. This section of the interface contains all the Core bindings of the program.

> ghc-9.6 --show-iface FAT.hi
....
extra-decls
f = GHC.Types.C# 'f'#
a = GHC.Types.C# 'a'#
t = GHC.Types.C# 't'#
....

The serialised program is a Core program. Using the Core representation is convenient for a number of reasons:

  • We already have the ability to serialise Core.
  • Constructing bytecode from Core is not a very expensive operation.
  • Other backends can generate code from the Core.

The program is serialised after simplification. This means that the interface file for a module compiled without optimisations will contain unoptimised bindings, whereas the interface file for an optimised module will contain optimised bindings.

Template Haskell evaluation via bytecode

GHC always uses the bytecode interpreter to interpret a Template Haskell splice for the current module. On the other hand, dependent home package modules can be handled in two different ways:

  • Object files: link the object files together using the system linker, and pass the resulting library to the interpreter.
  • Bytecode: directly load the already compiled bytecode into the interpreter.

By default, GHC in --make mode uses the former method, whereas GHCi uses the latter. GHC 9.6 introduces new flags to change this behaviour: -fbyte-code-and-object-code and -fprefer-byte-code.

Generating bytecode

In order to generate both the bytecode and object file linkables, there is a new flag -fbyte-code-and-object-code:

-fbyte-code-and-object-code

Produce both bytecode and object code for a module. This flag implies -fwrite-if-simplified-core.

Using -fbyte-code-and-object-code without -fwrite-if-simplified-core would recompile your project from scratch each time you compile it, due to lacking the Core definitions in the interface. Having one flag enable the other avoids this.

Compare -fbyte-code-and-object-code with the existing -fobject-code and -fbyte-code flags, which don’t allow a combination:

-fobject-code

Produce object code for a module. This flag turns off -fbyte-code-and-object-code so using -fobject-code in an OPTIONS_GHC pragma will ensure that bytecode is never produced or used for a module.

-fbyte-code

Produce bytecode for a module. This flag turns off -fbyte-code-and-object-code so using -fbyte-code means to only produce bytecode for a module.

When using -fbyte-code-and-object-code, the recompilation checker checks for the presence of an interface file with Core definitions, recompiling the module if one doesn’t exist.

If -fbyte-code-and-object-code is not enabled then even if you have an interface file with the Core program the bytecode isn’t loaded for a module. This prevents the situation where you first compile an interface for module A and then later recompile it with -fobject-code, then you don’t want to make the bytecode available for later modules if they use -fprefer-byte-code.

Using bytecode

When passed the new -fprefer-byte-code flag, GHC will use the bytecode interpreter whenever bytecode is available (including in --make mode).

-fprefer-byte-code

Use bytecode rather than object files for module dependencies when evaluating Template Haskell splices. This flag affects the decision we make about which linkable to use at the splice site only. It doesn’t have any effect on which linkables are generated from a module.

In addition, if you prefer bytecode, then the compiler will automatically turn on bytecode generation if it needs code generation when using -fno-code.

There are a couple of reasons why you might want to use these flags:

  • Producing object code is much slower than producing bytecode, and normally you need to compile with -dynamic-too to produce code in the static and dynamic way, the dynamic way just for Template Haskell execution when using a dynamically linked compiler.

  • Linking many large object files, which happens once per splice, can be very expensive compared to linking bytecode. Mercury saw an overall 40% decrease in compilation times when compiling their code base using -fprefer-byte-code due to the large amount of Template Haskell splices they use.

There’s also some reasons why you might not want to use these flage:

  • Enabling -fbyte-code-and-object-code generates bytecode as well as normal object files, so it could make your compilation slower if you are producing static object files, dynamic object files and bytecode for each module.

  • These flags will run the bytecode interpreter with optimised programs, something which wasn’t possible before GHC 9.6 so there are probably some lurking bugs. We have already fixed a large number of these issues but we’re not confident yet that we have found them all.

You probably want to use both -fprefer-byte-code and -fbyte-code-and-object-code together. If you use -fprefer-byte-code alone, then bytecode will not necessarily be available to use. If you use -fbyte-code-and-object-code alone, then the bytecode which you generate will never be used. This may not be an issue (as the bytecode is generated lazily), but it’s something to keep in mind.

Trying it out

In order to use the bytecode interpreter to evaluate the Template Haskell splices in your project, enable the necessary options with the following section in your cabal.project:

program-options
  ghc-options: -fprefer-byte-code -fbyte-code-and-object-code

This will pass these two options when compiling all the packages local to your project. If you want to always pass these options even when compiling external dependencies you can instead write:

package *
  ghc-options: -fprefer-byte-code -fbyte-code-and-object-code

Conclusion

Including the Core program in an interface file is a simple but powerful feature. To be maximally effective, more work is necessary in the ecosystem to use them when appropriate to restart compilation, but this contribution makes the important first steps. For example, HLS already implements a similar feature to improve reload times but in future GHC versions they can instead use this native support.

Well-Typed are actively looking for funding to continue maintaining and enhancing GHC, HLS and Cabal. If your company relies on Haskell, and you could support this work, or would like help improving the developer experience for your Haskell engineers, please get in touch with us via info@well-typed.com!