Inlining some primitives
Reconsider our model of a Lisp program as a “black box” that issues a series primitive function calls. We can eliminate some of the primitive function calls by implementing them directly within our “black box”. We inline some primitives.
Take null?
as an example. Instead of
constructing (null? arg)
as
[primitive-funcall1 [quote [primitive null?]] [argument 0]]
we add a new S-code construct, primitive-null?
, and
construct (null? arg)
as
[primitive-null? [argument 0]]
We don't have to evaluate the function. We don't even have to jump
to the entry point of the null?
primitive. After we
evaluate argument 0, we just test for null in the interpreter and
return T
or NIL
as appropriate.
There are maybe 20 or so primitives that are used frequently enough to be worth inlining in this way. Each primitive you inline like this adds bloat to the interpreter.
Inlining simple arguments
The leaves of a tree of S-code are the atomic expressions, whereas the internal nodes of the tree are compound expressions. We can eliminate the leaves by inlining them into their parent nodes. For example if a leaf node is a lexical variable reference, we inline this into the parent node. We unroll the evaluation of the leaf node thus saving a recursive call to the interpreter and an evaluation dispatch.
Consider our previous example which we consructed as
[primitive-null? [argument 0]]
We further specialize primitive-null?
based on its
argument type into primitive-null?-argument
or primitive-null?-lexical
. Now our S-code
becomes:
[primitive-null?-argument 0]
The leaf node [argument 0]
is absorbed
into the parent node [primitive-null? ...]
making a
new leaf node [primitive-null?-argument]
. The
evaluator for this S-code node simply tests if argument 0
is null and returns T
or NIL
as
appropriate.
Compare this to the original S-code:
[funcall [global 'null?] [argument 0]]
This required two recursive calls to the interpreter, a global
lookup, and a primitive function call on top of the null test.
We’ve eliminated all of those. There’s not much left to
do. Testing null?
in the interpreter is almost as
fast as testing null?
in compiled code.
The number of S-code types needed to perform this inlining is the number of kinds of leaf nodes raised to the power of the number of leaves in the parent node. A call to a one-argument primitive would need specializations for the cases where the argument is a quoted value, an argument, a lexical variable or a global variable — four specializations. Calls to a two-argument primitive turn into one of sixteen specializations — the product of four for each argument. A call to a three-argument primitive would turn into one of sixty-four specializations.
We can inline all the non-compound argument evaluations, both to primitive functions and to user-defined functions. In our S-code tree, we have removed all the leaf nodes and absorbed them into their parent nodes (which have now become new leaves). The interpreter is now quite a bit faster, although still not as fast as compiled code.
No comments:
Post a Comment