Caching one-time startup (and GNU Octave JIT)

David Malcolm dmalcolm@redhat.com
Wed Jan 1 00:00:00 GMT 2014


On Wed, 2014-01-22 at 10:44 -0500, David Malcolm wrote:
> I've been experimenting with adding libgccjit support to GNU Octave and
> I think I've found a flaw with the current libgccjit API - the lack of a
> way to cache one-time startup.
> 
> GNU Octave already has a JIT compiler, using LLVM.  Octave has a
> singleton class "jit_typeinfo" which performs one-time creation of the
> various Octave types and callbacks, for example creating tables of
> overloaded functions, mapping from (input type, function) pairs to
> optimized handlers.
> (See e.g.:
> http://octave.sourceforge.net/doxygen/html/d2/da9/jit-typeinfo_8cc_source.html#l01072
> the jit_typeinfo constructor tor, which performs this one-time
> startup).
> 
> Then, when a function or loop becomes "hot", it gets JIT-compiled from a
> parse-tree representation to a "jit_ir" representation, which then gets
> turned into LLVM IR, and thence machine code.
> 
> Visually:
> 
>   once:
>      create types, function wrappers, overload tables, etc (the
>      jit_typeinfo singleton)
> 
>   when a function becomes "hot":
>     parse tree -> jit_ir -> llvm -> machine code
> 
> In my experiment, I have a semi-working:
> 
>   when a function becomes "hot":
>     parse tree -> jit_ir -> libgccjit -> machine code
> 
> but I'm running up against the lack of a way of doing one-time startup
> that's shared between all instances of a compile.
> 
> Currently we have an idea of a context, and a compilation callback, and
> all of the "contextual" entities have a lifetime that's only within the
> compilation callback - you can't have e.g. a (gcc_jit_type *) that gets
> stored at startup for later use by other compilations within the same
> process.
> 
> This split into one-time startup vs per-hot function seems like a
> natural thing for a real-world JIT compiler to want to do.
> 
> How should libgccjit support this?   Some ideas:
> 
> (A) Nested contexts: change lifetimes so that entities have the same
> lifetime as the context they were created in, but can be used within
> "child contexts" (with nesting).
> 
> So you can do:
>    root_context: create the one-time stuff
>      child_context (for one function): create stuff for just
>         this function, using the stuff created in
>         "root_context".
> We could then perhaps eliminate result objects, so that e.g. you could
> actually compile stuff on the root_context, and use it in a
> child_context.
> 
> This would be one more entrypoint:
>   gcc_jit_context_new_child_context
> and it would make sense for anything that creates something that's
> returned to take an explicit context ptr as an arg, since that affects
> the lifetime of the returned thing.
> 
> [But how to implement? Perhaps change so that things become
> heap-allocated "memento" objects, with a "free" of them all happening in
> the release of the context (perhaps using obstack?).   Play it all back
> during the compilation hook, with 2-way mappings between the mememtos
> and the tree objects. Eventually we can (I hope) remove the mementos and
> work directly with gcc objects].
> 
> (B) Explicit separation between one-time init vs per-compile: perhaps
> have: gcc_jit_context (one-time init) vs gcc_jit_module
> (an instance of a compile on a context, roughly corresponding to a
> "Translation Unit").
> 
> (C) Something else?  Ideas?
> 
> I think typically you'd want to create types and imported functions
> *once*, and then create exported functions as a per-compile thing.  But
> I think people may want flexibility here e.g. to create some one-time
> functions at startup, and then use them in the per-hot-function code.
> So (A) may be superior to (B), perhaps.
> 
> Thoughts?  I'm most interested in what the API ought to look like [1]

As pointed out on IRC, if we go to a memento-based internal
implementation, we can kill off the need to have a callback, and the
client code can simply call the API directly.   This will solve another
issue I ran into with libgccjit in Octave, which is that liboctave is in
C++ with exception-handling, whereas libgccjit isn't exception-aware,
and thus if an C++ exception is thrown within the callback, there isn't
a good way to propagate that exception back out through all the layers.
So getting rid of the callback from the API seems worthwhile in itself,
and I think I'll attempt that next.

I thought of another place where I'm unhappy with the *lifetime* of
entities in the existing API: some JITs will want to have code-patching,
where you can place a "patch point" within code, which can be updated
later.  So e.g. you have code in a dynamic lanugage that's hot when
called on, say, a list of int objects, but you later getting
non-heterogeneous lists e.g. a mixture of int and float.  Then you can
have a JIT-compiled handler for lists with the type-guards to check for
int-ness, and the failure-handling of the type-guard can have a
patch-point, so that you can later dynamically patch in code to better
handle the new types that the code is seeing (and indeed, the
failure-handler typically actually patches itself).

So we'd want something like:
  gcc_jit_patchpoint *
  gcc_jit_function_add_patchpoint (some args);
though what the args looks like, and what you do with a patchpoint
object, I'm not sure at this time.

But to be useful the gcc_jit_patchpoint will need to have a lifetime
that exceeds the duration of the callback, hence I mention it now.

Dave



More information about the Jit mailing list