Skip to content

Qi Compiler Sync Sept 16 2022

Siddhartha Kasivajhula edited this page Dec 15, 2022 · 4 revisions

Scrutinizing Dependencies to Minimize Require Latency

Qi Compiler Sync Sept 16 2022

Adjacent meetings: Previous | Up | Next

Summary

We scrutinized the dependencies in the bindingspec library to see if the require latency (that is, the time taken on (require bindingspec)) could be reduced, and found some quick wins. We explored a number of tools to help with this along the way.

Background

Previously, we discovered that introducing the bindingspec dependency inflated Qi's require latency by about 37%. While the resulting value of 250ms for (require qi) was still in the normal range for Racket libraries, we were curious whether this latency could be minimized.

Investigation

The initial value was obtained from the require-latency raco tool:

$ raco require-latency bindingspec

(after compiling the bindingspec package via raco setup bindingspec)

We wanted a more transparent way to measure the time taken for the purposes of investigating it. Starting a REPL and then running (time ...) proved not to be a reliable way to measure require latency since, it turned out, the REPL already loads racket/init (and also xrepl) which includes a lot of modules, thus reducing the time taken for subsequent imports (which would not load or instantiate any dependencies already loaded).

We used:

$ racket -l racket/base -e "(time (dynamic-require 'bindingspec 0))"

(Note that the opaque second argument to dynamic-require controls whether the module is loaded only for runtime or also for other phases. It may be worth trying #f and (void) there to compare results).

This reported an initial value of about 276ms, which was consistent with the value reported by raco require-latency bindingspec on this machine.

To understand which dependencies were being loaded here, we tried a few different tools.

Tools

Racket Import Profiler

We used the profile-imports raco command:

$ raco profile-imports -l bindingspec

to get a sense of modules contributing to the load time. This gave a breakdown of the percentage of time contributed by each module, resembling:

Profiling 25 discovered modules. This will take approx 50.00s...
Timings:
* ...a/work/racket/bindingspec/private/runtime/syntax-classes.rkt (154.49ms, weight 0.158483)
* .../bindingspec/private/syntax/compile/nonterminal-expander.rkt (146.46ms, weight 0.150243)
* ...rk/racket/bindingspec/private/syntax/compile/syntax-spec.rkt (129.43ms, weight 0.132779)
* ...tha/work/racket/bindingspec/private/runtime/binding-spec.rkt (94.29ms, weight 0.096728)
* /Users/siddhartha/work/racket/ee-lib/main.rkt (92.37ms, weight 0.094756)
* ...dhartha/work/racket/bindingspec/private/syntax/interface.rkt (72.07ms, weight 0.073934)
* /Applications/Racket-Latest/collects/syntax/id-set.rkt (62.99ms, weight 0.064622)
* /Users/siddhartha/work/racket/ee-lib/persistent-id-table.rkt (56.58ms, weight 0.058046)
* ...ddhartha/work/racket/bindingspec/private/runtime/compile.rkt (38.56ms, weight 0.039555)
* /Users/siddhartha/work/racket/ee-lib/define.rkt (37.30ms, weight 0.038269)
* ...ha/work/racket/bindingspec/private/syntax/syntax-classes.rkt (28.91ms, weight 0.029655)
* ...ddhartha/work/racket/bindingspec/private/syntax/env-reps.rkt (27.75ms, weight 0.028468)
* /Applications/Racket-Latest/collects/syntax/parse.rkt (25.53ms, weight 0.026192)

The output appeared to include both runtime as well as compile-time dependencies. We were interested in further distinguishing the contributions from each.

Show Dependencies

raco show-dependencies main.rkt

This listed dependencies but didn't give a lot more information than that.

Module Browser

DrRacket's Module Browser (with "follow lib links" checked) showed the full tree of dependencies for bindingspec's main.rkt, and also showed what phase those dependencies were required at. We discovered, for instance, that as expected, syntax/parse was only required at phase 1 and above.

Check Requires

We tried the check-requires tool to see if it would report any dependencies that could be removed, but we weren't able to use it for our purposes:

$ raco check-requires bindingspec

bindingspec:
DROP syntax/parse at 1
DROP ee-lib at 1
[...]

This reported that syntax/parse could be dropped at phase 1, but that wasn't right. We concluded that this tool doesn't recognize empty provides (i.e. require and provide without otherwise using it in the module).

$ raco check-requires qi

qi:
ERROR in qi
-: contract violation
  expected: number?
  given: '(0 . qi)
  context...:
   /Applications/Racket-Latest/share/pkgs/macro-debugger-text-lib/macro-debugger/analysis/private/nom-use-alg.rkt:143:2
   /Applications/Racket-Latest/share/pkgs/macro-debugger-text-lib/macro-debugger/analysis/private/nom-use-alg.rkt:141:0: mod->bypass-table
[...]

produced an error. We concluded that the tool is not aware of binding spaces.

Using the Current Load Handler to Track Loaded Modules

racket -l racket/base -e "(let ([old (current-load/use-compiled)]) (current-load/use-compiled (lambda (p n) (displayln p) (old p n))))" -e "(time (dynamic-require 'bindingspec 0))"

produced a full list of all modules loaded by bindingspec, resembling:

/Users/siddhartha/work/racket/bindingspec/main.rkt
/Users/siddhartha/work/racket/bindingspec/private/syntax/interface.rkt
/Users/siddhartha/work/racket/bindingspec/private/runtime/errors.rkt
/Users/siddhartha/work/racket/bindingspec/private/runtime/compile.rkt
/Applications/Racket-Latest/collects/racket/private/check.rkt
/Applications/Racket-Latest/collects/racket/match.rkt
/Applications/Racket-Latest/collects/racket/match/match.rkt
/Applications/Racket-Latest/collects/racket/match/runtime.rkt
/Applications/Racket-Latest/collects/racket/match/match-expander.rkt
[...]

Analysis

The Module Lifecycle

There are several stages in the module lifecycle from when it is parsed to when it is required in another module:

  1. The module is expanded
  2. The module is compiled
  3. The compiled file is loaded (i.e. it is read from disk)
  4. The loaded file is evaluated or "instantiated"

Additionally, there is also the phase at which a module is required that matters for how much latency it contributes overall.

Dependencies at Different Phases

A module may require another module at runtime, or at another phase such as compile-time via (require (for-syntax ...)). The chain of dependencies and phases within a package determines what phases a particular dependent module is employed at in this package.

When a module is required at runtime, all of its dependencies are loaded from disk, but only the dependencies at the present phase are instantiated, and, in fact, these are instantiated lazily [TODO: verify].

Actions Taken

Cross-referencing the output from a combination of the tools above, we identified id-set.rkt as a dependency that was responsible for pulling in a lot of modules from racket/contract, and which could be avoided by using id-table and racket/list's remove-duplicates together with the free-identifier=? predicate, instead. This minimized the reliance on modules from racket/contract. Measuring the time again with $ racket -l racket/base -e "(time (dynamic-require 'bindingspec 0))", the time was now down from 276ms to about 189ms.

Next Steps

  • Continue exploring these various tools to try and reduce latency further, especially by instrumenting the instantiation part of the module lifecycle more.
  • Validate whether raco profile-imports reports both compile-time and runtime dependencies or just the latter. [Update 09/19: it can now do either. It defaults to both, and can limit to a maximum phase (e.g. 0 for runtime) via the -P flag]
  • Continue on prototyping compiler optimizations and bindings for Qi.

Attendees

Michael, Sid

Clone this wiki locally