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.INHIBIT-EVENT-CHECKING currently does nothing, and doesn't exist in some branches.
- POLICY.ALLOW-TRANSFORMS enables compiler macros. By default, his is true whenever safety, compilation-speed, and debug are less than 3 and speed is greater than 0.
- 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.
- POLICY.DECLARATIONS-TYPECHECK controls whether type-declarations are checked at runtime. It is true by default if SAFETY is 3 or SPEED is less than SAFETY, i.e. the opposite of the POLICY.TRUST-DECLARATIONS default. If it is true, the compiler will compile THE forms as runtime typechecks, and will also generate typechecks whenever variables of declared types are bound or setq'ed.
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.