Optimization Last updated June 14, 2022

Cross-module Optimization

The native compiler (with or without Flambda) enables cross-module optimization by default. The -opaque flag (Chapter 14 Native-code compilation (ocamlopt) disables it.

"Cross-module" optimization means compile-time optimization, not whole-program link-time optimization. For example inlining.

The meaning of -opaque is different for sigfiles and structfiles:

  • "When compiling [an] .mli interface, using -opaque marks the compiled .cmi interface so that subsequent compilations of modules that depend on it will not rely on the corresponding .cmx file, nor warn if it is absent."

  • "When the native compiler compiles a .ml implementation, using -opaque generates a .cmx that does not contain any cross-module optimization information."

In other words, using -opaque to compile a .mli file makes it act like a standalone signature: the compiler will ignore any corresponding .cmx file, which means the build system should not force a rebuild of modules that depend on the .cmi file even if the corresponding .ml file changes.

Similarly, if an .ml file is compiled with -opaque (and the .mli file is compiled without it), it won’t be ignored but it might as well be with respect to optimization, because its .cmx file will not have any optimization info.

In short, in case the .ml changes but the .mli does not, and the latter is compiled with -opaque, the .ml file should be recompiled, but this should not force a rebuild of anything that depends on the module. Remember that "depend on a module" means depend first on the .cmi file of the module, and then on the .cmx file.

An example (deps/opaque). We have the following dependency structure:

test (exe) > Test > Greeting > Hello

Now suppose we build hello.mli with -opaque.

If you change hello.ml and then build target //deps/opaque:Greeting, nothing will happen - in particular, hello.ml will not be recompiled. That’s because Bazel will only compile what is needed, and Greeting does not need hello.cmx - it does need hello.cmi, but that did not change, and because it was compiled with -opaque, it tells the compiler not to bother with hello.cmx.

Remember that you can build any target at the command line, no matter what its visibility attribute says. That attribute only governs visibility of dependencies.

However, if we build target //deps/opaque:test - the executable - then hello.ml will be compiled, and then the test executable will be built. But neither Greeting nor Test will be recompiled.

Something similar but slightly different happens when we compile an .ml file with -opaque. In this demo, Greeting is an "orphan" module - it only has greeting.ml, without a greeting.mli. That means greeting.cmi is dependent on greeting.ml, so it will be regenerated and recompiled whenever greeting.ml changes. In this situation there is no way to tell OCaml to compile just the .cmi file with -opaque. So we compile greeting.ml with -opaque. Then we can change greeting.ml without triggering a recompile of Test. However, building //deps/opaque:Test at the command line will recompile greeting.ml, even though Test does not need it. That’s because Test does need greeting.cmi, and since greeting.ml changed, it must be recompiled in order to produce greeting.cmi. And again if we build //deps/opaque:test, then greeting.ml will be recompiled and test will be relinked, but Test will not be recompiled.

If you run the demo, pass --subcommands=pretty_print to see what actually gets done.

Bazel can handle this with relative ease because it makes a distinction between target dependencies and action dependencies. When you write your BUILD.bazel file you decide what to list as target dependencies, but the rule implementation gets to decide what goes into the action dependency graph. And its the action dependency graph that determines what gets rebuilt.

When we build without -opaque, the ocaml_module rule puts all target dependencies in the (compile) action dependency graph, so changes will always trigger rebuilds all the way up the dependency chain. But when -opaque is involved, the OBazl ruies can figure out that the .cmx file should not go in the action depencency graph. Well, not the compile dependency graph. We also have a link dependency graph, and we always include everything in that, but it only gets added to the graph of executable targets. The OBazl rules always propagate these graphs up the dependency chain. This is necessary, because all inputs and outputs for an action must always be explicitly enumerated. This is a major difference between Bazel and (many) other build systems that just keep track of directories. That makes sense insofar as compilers generally accept directories and search them. But directories are not enough for Bazel.

Flambda

Libraries v. Archives

PPX Optimizations

  • shared ppx executables

  • single driver

  • mixed-mode compilation