Optimization Passes

LLVM provides many optimization passes, which do many different sorts of things and have different trade-offs. Unlike other systems, LLVM doesn’t hold to the mistaken notion that one set of optimizations is right for all languages and for all situations. LLVM allows a compiler implementor to make complete decisions about what optimizations to use, in which order, and in what situation.

As a concrete example, LLVM supports both “whole module” passes, which look across as large of body of code as they can (often a whole file, but if run at link time, this can be a substantial portion of the whole program). It also supports and includes “per-function” passes which just operate on a single function at a time, without looking at other functions. For more information on passes and how they are run, see the How to Write a Pass document and the List of LLVM Passes.

For Kaleidoscope, we are currently generating functions on the fly, one at a time, as the user types them in. We aren’t shooting for the ultimate optimization experience in this setting, but we also want to catch the easy and quick stuff where possible.

We won’t delve too much into the details of the passes since they are better described elsewhere. We will instead just invoke the default “curated passes” with an optimization level which will perform most of the common clean-ups and a few non-trivial optimizations.

  1. passes :: PassSetSpec
  2. passes = defaultCuratedPassSetSpec { optLevel = Just 3 }

To apply the passes we create a bracket for a PassManager and invoke runPassManager on our working module. Note that this modifies the module in-place.

  1. runJIT :: AST.Module -> IO (Either String AST.Module)
  2. runJIT mod = do
  3. withContext $ \context ->
  4. runExceptT $ withModuleFromAST context mod $ \m ->
  5. withPassManager passes $ \pm -> do
  6. runPassManager pm m
  7. optmod <- moduleAST m
  8. s <- moduleLLVMAssembly m
  9. putStrLn s
  10. return optmod

With this in place, we can try our test above again:

  1. ready> def test(x) (1+2+x)*(x+(1+2));
  2. ; ModuleID = 'my cool jit'
  3. ; Function Attrs: nounwind readnone
  4. define double @test(double %x) #0 {
  5. entry:
  6. %0 = fadd double %x, 3.000000e+00
  7. %1 = fmul double %0, %0
  8. ret double %1
  9. }
  10. attributes #0 = { nounwind readnone }

As expected, we now get our nicely optimized code, saving a floating point add instruction from every execution of this function. We also see some extra metadata attached to our function, which we can ignore for now, but is indicating certain properties of the function that aid in later optimization.

LLVM provides a wide variety of optimizations that can be used in certain circumstances. Some documentation about the various passes is available, but it isn’t very complete. Another good source of ideas can come from looking at the passes that Clang runs to get started. The “opt” tool allows us to experiment with passes from the command line, so we can see if they do anything.

One important optimization pass is an “analysis pass” which will validate that the internal IR is well-formed. Since it quite possible (even easy!) to construct nonsensical or unsafe IR it is very good practice to validate our IR before attempting to optimize or execute it. To do so, we simply invoke the verify function with our active module.

  1. runJIT :: AST.Module -> IO (Either String AST.Module)
  2. runJIT mod = do
  3. ...
  4. withPassManager passes $ \pm -> do
  5. runExceptT $ verify m

Now that we have reasonable code coming out of our front-end, let’s talk about executing it!