How Optimize Declarations are Implemented and What They Do
CCL uses a "policy object" to control how various speed/space/safety
tradeoffs are made; a "policy object" is just a structure-like thing
whose slots contain functions, most of which (there are a few
exceptions) take a lexical environment as an argument. There are
about a dozen such functions, and there are simple macros -
POLICY.ALLOW-TAIL-RECURSION-ELIMINATION, POLICY.OPEN-CODE-INLINE,
etc - that can be used to access these functions. There are separate
policy objects used for interactive compilation and for COMPILE-FILE;
all are equivalent by default, but the idea is that people might want
to be more aggressive when compiling a file than when defining things
interactively, or might not.
-Most- speed/safety/space/debugging tradeoffs are made by calling
a function in the current/active policy object with the lexical
environment as an argument; the function typically returns a boolean
result based on the values of the OPTIMIZE quantities in that
environment. For instance, a tail-call is optimized (the caller's
stack frame is reused) if the POLICY.ALLOW-TAIL-RECURSION-ELIMINATION
function in the current policy returns true. The value of this function
could be something like:
#'(lambda (env)
(< (debug-optimize-quantity env) 2))
so tail-call optimization will happen (under a policy with this
function) when DEBUG is less than 2.
Other functions contained in these policy objects
include
- POLICY.OPEN-CODE-INLINE controls whether certain fairly
long instruction sequences are done inline or not. One case
of this involves special variable reference: it takes around 8
instructions to determine whether a special variable has a
thread-local binding (and whether or not it's bound to a
real value or unbound), and a call/return to a runtime routine
(a "subprimitive") adds a few cycles of insult to injury. If the
lisp kernel profile shows a lot of time spent in _SPspecrefcheck,
it might help to spend less time doing special variable references
inline. The default policy function says that things should
be inlined when speed is greater than space; I can't think of
too many cases besides special variable reference where this
matters too much on the x86-64. (Fixnum operations that produce
bignum results might create the bignum inline or out-of-line, but
if this happens that often you're probably losing anyway.)
- POLICY.INHIBIT-SAFETY-CHECKING can make correct code faster and
incorrect code crash. When this is true (by default, only when
SPEED is 3 and SAFETY is 0), functions that take a fixed number
of arguments assume that they got the correct number of arguments
and can crash spectacularly if that's not true; if your program is
a lot of mostly-small, simple methods and functions calling each
other, then number-of-args checking is probably a small but
measurable source of overhead. Other things that can happen in
unsafe code involve the elimination of some type and bounds-
checking, often involving array references where the compiler has
some hint/clue as to the array's type.
- POLICY.TRUST-DECLARATIONS controls whether type-declarations
are used; it is true by default when SAFETY is less than 3 and SPEED
is greater or equal to SAFETY. If the compiler trusts type
declarations, then different things can happen depending on the
type in question:
-- for some types, the declaration is "blindly" trusted: if
X and Y are declared to be FIXNUMs and declarations are trusted,
then arithmetic operations involving X and Y will try to do
fixnum arithmetic without typechecking. (Part of the rationale
is that the typechecking in that case is likely to be more
expensive than the arithmetic); unsafe code can also be
generated for operations like CAR and CDR on values declared
to be LISTs.
-- for other types and primitive operations on them, trusted
declarations might influence the compiler to generate type-specific
code, but code is still safe ("trust, but verify") unless
POLICY.INHIBIT-SAFETY-CHECKING suggests otherwise.
With the exception of cases involving FIXNUM and LIST declarations,
code compiled at default optimization settings is generally as
safe as code compiled at high safety; in practice, people don't
seem to often declare things to be FIXNUMs or LISTs unless they're
pretty sure that that's the case. (Some of the FIXNUM/LIST stuff
is historical: it was probably once several times more expensive
to be safe than unsafe; now it is probably only 2 or 3 times as
expensive.)
SAFETY 3 is generally implemented by forcing the compiler to be
stupidly conservative about practically everything: compiler macros
aren't expanded, function calls are (almost always) compiled as
function calls (even if they involve primitive operations that
could be performed safely.) I'd like to try to get away from
this implementation strategy, but it's currently the case that
SAFETY 3 sacrifices (in some cases) a lot of performance for
what's in practice only occasionally more safety than is available
under default settings.
Skipping number-of-args checks can lead to horrible hard crashes
that can be hard to debug, partly because stack discipline may
be violated. When the callee is constant and the arguments
are explicit, mismatches can be warned about; if FUNCALL and
APPLY are used relatively rarely, then the risk of an undetected
number-of-args error is (in some sense) small.