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.
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:
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:
In order to generate both the bytecode and object file linkables, there is a new flag
Produce both bytecode and object code for a module. This flag implies
-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.
-fbyte-code-and-object-code with the existing
-fbyte-code flags, which don’t allow a combination:
Produce object code for a module. This flag turns off
OPTIONS_GHCpragma will ensure that bytecode is never produced or used for a module.
Produce bytecode for a module. This flag turns off
-fbyte-codemeans to only produce bytecode for a module.
-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.
-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
When passed the new
-fprefer-byte-code flag, GHC will use the bytecode interpreter whenever bytecode is available (including in
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
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-tooto 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-codedue to the large amount of Template Haskell splices they use.
There’s also some reasons why you might not want to use these flage:
-fbyte-code-and-object-codegenerates 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
-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
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
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 firstname.lastname@example.org!