ack/doc/ego/il/il5
1990-06-20 10:05:22 +00:00

447 lines
14 KiB
Plaintext

.NH 2
Implementation
.PP
A major factor in the implementation
of Inline Substitution is the requirement
not to use an excessive amount of memory.
IL essentially analyzes the entire program;
it makes decisions based on which procedure calls
appear in the whole program.
Yet, because of the memory restriction, it is
not feasible to read the entire program
in main memory.
To solve this problem, the IL phase has been
split up into three subphases that are executed sequentially:
.IP 1.
analyze every procedure; see how it accesses its parameters;
simultaneously collect all calls
appearing in the whole program an put them
in a \fIcall-list\fR.
.IP 2.
use the call-list and decide which calls will be substituted
in line.
.IP 3.
take the decisions of subphase 2 and modify the
program accordingly.
.LP
Subphases 1 and 3 scan the input program; only
subphase 3 modifies it.
It is essential that the decisions can be made
in subphase 2
without using the input program,
provided that subphase 1 puts enough information
in the call-list.
Subphase 2 keeps the entire call-list in main memory
and repeatedly scans it, to
find the next best candidate for expansion.
.PP
We will specify the
data structures used by IL before
describing the subphases.
.NH 3
Data structures
.NH 4
The procedure table
.PP
In subphase 1 information is gathered about every procedure
and added to the procedure table.
This information is used by the heuristic rules.
A proctable entry for procedure p has
the following extra information:
.IP -
is it allowed to substitute an invocation of p in line?
.IP -
is it allowed to put any parameter of such a call in line?
.IP -
the size of p (number of EM instructions)
.IP -
does p 'fall through'?
.IP -
a description of the formal parameters that p accesses; this information
is obtained by looking at the code of p. For every parameter f,
we record:
.RS
.IP -
the offset of f
.IP -
the type of f (word, double word, pointer)
.IP -
may the corresponding actual parameter be put in line?
.IP -
is f ever accessed indirectly?
.IP -
if f used: never, once or more than once?
.RE
.IP -
the number of times p is called (see below)
.IP -
the file address of its call-count information (see below).
.LP
.NH 4
Call-count information
.PP
As a result of Inline Substitution, some procedures may
become useless, because all their invocations have been
substituted in line.
One of the tasks of IL is to keep track which
procedures are no longer called.
Note that IL is especially keen on procedures that are
called only once
(possibly as a result of expanding all other calls to it).
So we want to know how many times a procedure
is called \fIduring\fR Inline Substitution.
It is not good enough to compute this
information afterwards.
The task is rather complex, because
the number of times a procedure is called
varies during the entire process:
.IP 1.
If a call to p is substituted in line,
the number of calls to p gets decremented by 1.
.IP 2.
If a call to p is substituted in line,
and p contains n calls to q, then the number of calls to q
gets incremented by n.
.IP 3.
If a procedure p is removed (because it is no
longer called) and p contains n calls to q,
then the number of calls to q gets decremented by n.
.LP
(Note that p may be the same as q, if p is recursive).
.sp 0
So we actually want to have the following information:
.DS
NRCALL(p,q) = number of call to q appearing in p,
for all procedures p and q that may be put in line.
.DE
This information, called \fIcall-count information\fR is
computed by the first subphase.
It is stored in a file.
It is represented as a number of lists, rather than as
a (very sparse) matrix.
Every procedure has a list of (proc,count) pairs,
telling which procedures it calls, and how many times.
The file address of its call-count list is stored
in its proctable entry.
Whenever this information is needed, it is fetched from
the file, using direct access.
The proctable entry also contains the number of times
a procedure is called, at any moment.
.NH 4
The call-list
.PP
The call-list is the major data structure use by IL.
Every item of the list describes one procedure call.
It contains the following attributes:
.IP -
the calling procedure (caller)
.IP -
the called procedure (callee)
.IP -
identification of the CAL instruction (sequence number)
.IP -
the loop nesting level; our heuristic rules appreciate
calls inside a loop (or even inside a loop nested inside
another loop, etc.) more than other calls
.IP -
the actual parameter expressions involved in the call;
for every actual, we record:
.RS
.IP -
the EM code of the expression
.IP -
the number of bytes of its result (size)
.IP -
an indication if the actual may be put in line
.RE
.LP
The structure of the call-list is rather complex.
Whenever a call is expanded in line, new calls
will suddenly appear in the program,
that were not contained in the original body
of the calling subroutine.
These calls are inherited from the called procedure.
We will refer to these invocations as \fInested calls\fR
(see Fig. 5.1).
.DS
.TS
lw(2.5i) l.
procedure p is
begin .
a(); .
b(); .
end;
.TE
.TS
lw(2.5i) l.
procedure r is procedure r is
begin begin
x(); x();
p(); -- in line a(); -- nested call
y(); b(); -- nested call
end; y();
end;
.TE
Fig. 5.1 Example of nested procedure calls
.DE
Nested calls may subsequently be put in line too
(probably resulting in a yet deeper nesting level, etc.).
So the call-list does not always reflect the source program,
but changes dynamically, as decisions are made.
If a call to p is expanded, all calls appearing in p
will be added to the call-list.
.sp 0
A convenient and elegant way to represent
the call-list is to use a LISP-like list.
.[
poel lisp trac
.]
Calls that appear at the same level
are linked in the CDR direction. If a call C
to a procedure p is expanded,
all calls appearing in p are put in a sub-list
of C, i.e. in its CAR.
In the example above, before the decision
to expand the call to p is made, the
call-list of procedure r looks like:
.DS
(call-to-x, call-to-p, call-to-y)
.DE
After the decision, it looks like:
.DS
(call-to-x, (call-to-p*, call-to-a, call-to-b), call-to-y)
.DE
The call to p is marked, because it has been
substituted.
Whenever IL wants to traverse the call-list of some procedure,
it uses the well-known LISP technique of
recursion in the CAR direction and
iteration in the CDR direction
(see page 1.19-2 of
.[
poel lisp trac
.]
).
All list traversals look like:
.DS
traverse(list)
{
for (c = first(list); c != 0; c = CDR(c)) {
if (c is marked) {
traverse(CAR(c));
} else {
do something with c
}
}
}
.DE
The entire call-list consists of a number of LISP-like lists,
one for every procedure.
The proctable entry of a procedure contains a pointer
to the beginning of the list.
.NH 3
The first subphase: procedure analysis
.PP
The tasks of the first subphase are to determine
several attributes of every procedure
and to construct the basic call-list,
i.e. without nested calls.
The size of a procedure is determined
by simply counting its EM instructions.
Pseudo instructions are skipped.
A procedure does not 'fall through' if its CFG
contains a basic block
that is not the last block of the CFG and
that ends on a RET instruction.
The formal parameters of a procedure are determined
by inspection of
its code.
.PP
The call-list in constructed by looking at all CAL instructions
appearing in the program.
The call-list should only contain calls to procedures
that may be put in line.
This fact is only known if the procedure was
analyzed earlier.
If a call to a procedure p appears in the program
before the body of p,
the call will always be put in the call-list.
If p is later found to be unsuitable,
the call will be removed from the list by the
second subphase.
.PP
An important issue is the recognition
of the actual parameter expressions of the call.
The front ends produces messages telling how many
bytes of formal parameters every procedure accesses.
(If there is no such message for a procedure, it
cannot be put in line).
The actual parameters together must account for
the same number of bytes.A recursive descent parser is used
to parse side-effect free EM expressions.
It uses a table and some
auxiliary routines to determine
how many bytes every EM instruction pops from the stack
and how many bytes it pushes onto the stack.
These numbers depend on the EM instruction, its argument,
and the wordsize and pointersize of the target machine.
Initially, the parser has to recognize the
number of bytes specified in the formals-message,
say N.
Assume the first instruction before the CAL pops S bytes
and pushes R bytes.
If R > N, too many bytes are recognized
and the parser fails.
Else, it calls itself recursively to recognize the
S bytes used as operand of the instruction.
If it succeeds in doing so, it continues with the next instruction,
i.e. the first instruction before the code recognized by
the recursive call, to recognize N-R more bytes.
The result is a number of EM instructions that collectively push N bytes.
If an instruction is come across that has side-effects
(e.g. a store or a procedure call) or of which R and S cannot
be computed statically (e.g. a LOS), it fails.
.sp 0
Note that the parser traverses the code backwards.
As EM code is essentially postfix code, the parser works top down.
.PP
If the parser fails to recognize the parameters, the call will not
be substituted in line.
If the parameters can be determined, they still have to
match the formal parameters of the called procedure.
This check is performed by the second subphase; it cannot be
done here, because it is possible that the called
procedure has not been analyzed yet.
.PP
The entire call-list is written to a file,
to be processed by the second subphase.
.NH 3
The second subphase: making decisions
.PP
The task of the second subphase is quite easy
to understand.
It reads the call-list file,
builds an incore call-list and deletes every
call that may not be expanded in line (either because the called
procedure may not be put in line, or because the actual parameters
of the call do not match the formal parameters of the called procedure).
It assigns a \fIpay-off\fR to every call,
indicating how desirable it is to expand it.
.PP
The subphase repeatedly scans the call-list and takes
the call with the highest ratio.
The chosen one gets marked,
and the call-list is extended with the nested calls,
as described above.
These nested calls are also assigned a ratio,
and will be considered too during the next scans.
.sp 0
After every decision the number of times
every procedure is called is updated, using
the call-count information.
Meanwhile, the subphase keeps track of the amount of space left
available.
If all space is used, or if there are no more calls left to
be expanded, it exits this loop.
Finally, calls to procedures that are called only
once are also chosen.
.PP
The actual parameters of a call are only needed by
this subphase to assign a ratio to a call.
To save some space, these actuals are not kept in main memory.
They are removed after the call has been read and a ratio
has been assigned to it.
So this subphase works with \fIabstracts\fR of calls.
After all work has been done,
the actual parameters of the chosen calls are retrieved
from a file,
as they are needed by the transformation subphase.
.NH 3
The third subphase: doing transformations
.PP
The third subphase makes the actual modifications to
the EM text.
It is directed by the decisions made in the previous subphase,
as expressed via the call-list.
The call-list read by this subphase contains
only calls that were selected for expansion.
The list is ordered in the same way as the EM text,
i.e. if a call C1 appears before a call C2 in the call-list,
C1 also appears before C2 in the EM text.
So the EM text is traversed linearly,
the calls that have to be substituted are determined
and the modifications are made.
If a procedure is come across that is no longer needed,
it is simply not written to the output EM file.
The substitution of a call takes place in distinct steps:
.IP "change the calling sequence" 7
.sp 0
The actual parameter expressions are changed.
Parameters that are put in line are removed.
All remaining ones must store their result in a
temporary local variable, rather than
push it on the stack.
The CAL instruction and any ASP (to pop actual parameters)
or LFR (to fetch the result of a function)
are deleted.
.IP "fetch the text of the called procedure"
.sp 0
Direct disk access is used to to read the text of the
called procedure.
The file offset is obtained from the proctable entry.
.IP "allocate bytes for locals and temporaries"
.sp 0
The local variables of the called procedure will be put in the
stack frame of the calling procedure.
The same applies to any temporary variables
that hold the result of parameters
that were not put in line.
The proctable entry of the caller is updated.
.IP "put a label after the CAL"
.sp 0
If the called procedure contains a RET (return) instruction
somewhere in the middle of its text (i.e. it does
not fall through), the RET must be changed into
a BRA (branch), to jump over the
remainder of the text.
This label is not needed if the called
procedure falls through.
.IP "copy the text of the called procedure and modify it"
.sp 0
References to local variables of the called routine
and to parameters that are not put in line
are changed to refer to the
new local of the caller.
References to in line parameters are replaced
by the actual parameter expression.
Returns (RETs) are either deleted or
replaced by a BRA.
Messages containing information about local
variables or parameters are changed.
Global data declarations and the PRO and END pseudos
are removed.
Instruction labels and references to them are
changed to make sure they do not have the
same identifying number as
labels in the calling procedure.
.IP "insert the modified text"
.sp 0
The pseudos of the called procedure are put after the pseudos
of the calling procedure.
The real text of the callee is put at
the place where the CAL was.
.IP "take care of nested substitutions"
.sp 0
The expanded procedure may contain calls that
have to be expanded too (nested calls).
If the descriptor of this call contains actual
parameter expressions,
the code of the expressions has to be changed
the same way as the code of the callee was changed.
Next, the entire process of finding CALs and doing
the substitutions is repeated recursively.
.LP