Shared libraries and GHC
[edit]
GHC can compile code to shared libraries. These are object files that are linked dynamically at runtime, and can be shared between several executables, potentially reducing disk usage and memory footprint.
However, libraries compiled with GHC do not provide a stable ABI (application binary interface), so one cannot update a dependency of a dynamically linked executable by simply dropping in a new version of the corresponding shared library. The primary reason for this is that the compiler relies heavily on cross-module (and cross-library) inlining in order to produce efficient code.
Benefits
- faster loading in ghci (at least under Linux), due to a more efficient linker
- sharing may reduce memory and disk space usage
Downsides
- no backward compatible libraries (this is a problem with rolling release distributions like Arch Linux, where a system update may break dynamically linked Haskell executables like xmonad)
- shared libraries include all code from a library, even if it is unused
Why inlining matters for performance[edit]
Lazy evaluation for languages like Haskell is accomplished by using a graph reduction engine instead of normal evaluation. This is effective and about as efficient as you can get when a majority of values are not strictly evaluated. However, it is only relatively efficient: evaluation jumps around a lot more than in straightforward procedural or OO code, and often does indirect jumps. In addition, GHC implements Haskell typeclasses as a runtime dictionary of type-specific operations which can require double indirect jumps (one for the dictionary itself, the other for the type-specific function/typeclass method). (The only time the STG graph reduction engine used by GHC does a call instead of a jump is garbage collection, or calls to foreign libraries.) In addition, because of lazy evaluation, all values are indirect and may require additional indirect jumps to force evaluation to the next constructor (or, depending on context, the entire value).
All this jumping and indirect jumping carries a cost. In some cases involving typeclass dictionaries, this cost can be extremely severe: one recent pathological case saw a 90x slowdown when inlining was disabled. 5-10x is more common, but 20x is not especially unusual.
GHC tries to inline code whenever possible, and in particular tries to resolve typeclass methods at compile time to avoid the dictionary lookup. This doesn't help evaluation across modules, however. To deal with this, GHC exports some of each module's internal code for inlining in the .hi
(Haskell Interface) file. When compiling another module with inlining enabled, it can inline the code from the interface file for that module instead of having to jump to it. This can also mean that a double indirect jump through a typeclass dictionary can be optimized to a direct jump, or even avoid the extra jumps entirely.
Consequences[edit]
Inlining means that the binary interface (ABI) to a module includes exported inlinable code. If you change the code in a module, you must recompile the modules that use it in case they have inlined some of that code, or you risk errors ranging from bizarre results to crashes.
This means that Haskell shared libraries have to carry an ABI hash incorporating the contents of the .hi
file, and that hash will usually change if you change the source code to a module unless you happen to only change things that are self-recursive or otherwise can't be inlined. So the primary reason you would want to use a Haskell shared library — being able to drop in a bug fix without rebuilding consumers of that library — is more or less impossible. (There are other reasons you might need a Haskell shared library, mostly related to run-time loading for things like Template Haskell splices.)
NOTE: Until recently, GHC shared libraries had an additional problem that precluded their use even with careful build systems such as Nix: they embedded compiler temporary names into the shared library, again altering the ABI but on every build even of an identical library with identical options. This has been fixed, but it does nothing about the more general issue with cross-module inlining. And it is unlikely that cross-module inlining will ever go away because of the severe performance penalties described above.