;;; 001-sbcl-vops.txt
On the nature of the :from and :to options for VOP operands, and
the behavior of :target.
Alastair Bridgewater, August 2006
The :from and :to options of the operands and temporaries of a
VOP control the lifetimes of the associated TNs, and thus their
register packing. The :target option is used to indicate that
two TNs should be packed into the same location. Careful use of
these options will allow one to reduce the number of registers
requred for a VOP to the minimum.
Each operand (argument or result), and each temporary defined in
a VOP is associated with a TN.
A time-spec indicates when a TN is born (the :from option) or
dies (the :to option). Two TNs cannot coexist in the same place
at the same time, so if you want an argument to be able to pack
to the same register as a temporary, you must have the temporary
be born at some point after the death of the argument.
A time-spec consists of a phase and a sub-phase. The phase is
one of the keywords :load, :argument, :eval, :result, or :save
(in that order, timewise). The sub-phase is a positive integer
used for ordering births and deaths within a phase. A time-spec
can either be a list (phase sub-phase), or just the keyword for
the phase (which is treated as (phase 0)).
An temporary has both :from and :to time-specs, describing its
entire lifetime. An argument has only a :to time-spec, and is
treated as being born before the VOP starts. A result has only
a :from time-spec, and is treated as dying after the VOP ends.
The default time-specs for a temporary are :from :load :to
:save. This means that the temporary is live for the entire
VOP. The default :to time-spec for the Nth argument is
(:argument N). The default :from time-spec for the Nth result
is (:result N).
The :target option tells the system to pack a TN at the same
offset within an SB as another TN if their lifetimes don't
overlap, no other TN gets there first, and the moon is waxing
gibbous (waning gibbous, in SBCL prior to 0.8.9). Fortunately,
the only TNs which can conflict are all under our control, being
the operands and temporaries for the VOP.
An example will hopefully make this all a little clearer. Let's
suppose we have a VOP that takes two arguments, called arg0 and
arg1, and has a temporary called edx which is set :sc any-reg
:offset edx-offset. We would like arg1 to pack in the same
location as edx, but adding :target edx to arg1 isn't doing the
trick, because the lifetimes aren't set up correctly. The
default lifetimes and relevant part of the VOP definition are as
follows:
:load (:argument 0) (:argument 1) :eval --//-- :save
arg0: -------------|
arg1: ----------------------------|
edx: |-------------------------------------------------|
(:args (arg0 :scs (descriptor-reg))
(arg1 :scs (descriptor-reg) :target edx))
(:temporary (:sc descriptor-reg :offset edx-offset) edx)
The first thing to do is to change edx to be :from (:argument
1), which means its lifetime no longer overlaps arg1:
:load (:argument 0) (:argument 1) :eval --//-- :save
arg0: -------------|
arg1: ----------------------------|
edx: |-----------------------|
(:args (arg0 :scs (descriptor-reg))
(arg1 :scs (descriptor-reg) :target edx))
(:temporary (:sc descriptor-reg :offset edx-offset
:from (:argument 1)) edx)
At this point, however, the compiler will gleefully pack arg0
into the same location as edx despite the :target option.
Whoops. To prevent this from happening, we change arg0 to be
:to :eval, and we have the following:
:load (:argument 0) (:argument 1) :eval --//-- :save
arg0: ---------------------------------------|
arg1: ----------------------------|
edx: |-----------------------|
(:args (arg0 :scs (descriptor-reg) :to :eval)
(arg1 :scs (descriptor-reg) :target edx))
(:temporary (:sc descriptor-reg :offset edx-offset
:from (:argument 1)) edx)
At this point, arg1 is reliably packed in the same register as
edx, and the VOP will use as few registers as possible.
Hopefully this all makes sense now.
;;; EOF