[Top][All Lists]

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

tracepoints/breakpoints and native compilation

From: Andy Wingo
Subject: tracepoints/breakpoints and native compilation
Date: Wed, 09 May 2018 09:34:39 +0200
User-agent: Gnus/5.13 (Gnus v5.13) Emacs/25.3 (gnu/linux)

Hi :)

A design question for everyone.  I am wondering how to support
breakpoints, tracepoints, and the like in a Guile with native-code
compilation.  If you are not familiar with what Guile does currently,

Basically Guile supports calling out to user-defined procedures when:

  (1) pushing a new continuation (stack frame)
  (2) tail-calling a procedure
  (3) popping a continuation (e.g. on return)
  (4) non-local return (after abort, or after calling a call/cc continuation)
  (5) advancing the instruction pointer

This last one is obviously expensive.  To mitigate this cost, Guile
builds the VM twice: once with hooks and once without.  When run with
--debug, you get the VM with hooks; otherwise, no hooks for you.

Note that without hooks, you still get interrupts, so you can do
statistical profiling, or interrupt a loop.  But unlike hooks,
interrupts are placed in the code.  Practically speaking, without hooks,
what you lose is the ability to set a breakpoint.  You also lose the
ability to trace a procedure, but because tracing can recursively invoke
the right VM, that's not a problem in practice.

Generally you only care about hook performance when they are disabled,
so what you need to do is minimize overhead in that case.  In a bytecode
interpreter (as Guile has now), the hooks do add some overhead, but they
are very predictable branches, so it's not a big deal.  Debug mode is on
by default in the Guile REPL and I think it adds some 10 or 20%

However in native-code compilation, hooks are more of a problem.  They
increase the executable size, as each hook invocation has to be present
in the text somewhere.  They pollute the branch predictor, as there are
many more branches.  They increase instruction cache size, even in the
best case where the slow branches are all out of line.  Boo.

So... I have a proposal.  Right now in the short term I am going to make
a JIT for Guile 3.0 (master branch) bytecode.  I am still working on
massaging the bytecode into a state where it is very easily JITtable,
but that will be soon I hope (weeks) and then, outside circumstances
permitting, we can have a JIT within the next 3 months.  For JIT
compilation, the "hook" problem resolves itself very easily: we can JIT
in two modes, one that adds hooks and one that doesn't.  We can alter
the hook API to allow the VM to know when to re-JIT code; adding a
breakpoint doesn't have to re-JIT everything.  Or of course we can keep
the current API and just re-JIT everything.

For the bytecode interpreter, we can keep the two VMs.  Also easy.

However if we ship native code in the .go files -- what do we do?  Three
options, I see -- one, ship "regular" code (no hooks) -- fast, but no
breakpoints from Guile.  Two, ship "debug" code (with hooks).  Or three,
ship the bytecode also, and a JIT compiler, so that we can re-JIT if

The first possibility would mean that some Guile-compiled code is not
really debuggable in a nice way.  It's not ideal.  The second is OK, but
it would be slow.  However the third option seems to offer a good choice
for general-purpose installs.  If we we ship the bytecode in .go files
by default and the built Guile supports JIT compilation, then maybe we
can get all the advantages -- peak performance with native code without
embedded hook calls, but still the ability to insert hooks if needed.

So I am looking at going with the third option.  It also opens the door
to potential experiments with trace compilation and optimization.  I am
currently not looking at adaptive optimization otherwise, but anyway
that's a bit far off; we need native compilation first to get good
startup time.

Thoughts welcome!


reply via email to

[Prev in Thread] Current Thread [Next in Thread]