mirror of
git://git.code.sf.net/p/sbcl/sbcl
synced 2026-09-10 07:26:40 -04:00
538 lines
26 KiB
TeX
538 lines
26 KiB
TeX
% -*- Dictionary: design; Package: C -*-
|
||
|
||
\#|
|
||
\chapter{Debugger Information}
|
||
\index{debugger information}
|
||
\label{debug-info}
|
||
|
||
Although the compiler's great freedom in choice of function call conventions
|
||
and variable representations has major efficiency advantages, it also has
|
||
unfortunate consequences for the debugger. The debug information that we need
|
||
is even more elaborate than for conventional "compiled" languages, since we
|
||
cannot even do a simple backtrace without some debug information. However,
|
||
once having gone this far, it is not that difficult to go the extra distance,
|
||
and provide full source level debugging of compiled code.
|
||
|
||
Full debug information has a substantial space penalty, so we allow different
|
||
levels of debug information to be specified. In the extreme case, we can
|
||
totally omit debug information.
|
||
|
||
|
||
\section{The Debug-Info Structure}
|
||
\index{debug-info structure}
|
||
|
||
The Debug-Info structure directly represents information about the
|
||
source code, and points to other structures that describe the layout of
|
||
run-time data structures.
|
||
|
||
|
||
Make some sort of minimal debug-info format that would support at least the
|
||
common cases of level 1 (since that is what we would release), and perhaps
|
||
level 0. Actually, it seems it wouldn't be hard to crunch nearly all of the
|
||
debug-function structure and debug-info function map into a single byte-vector.
|
||
We could have an uncrunch function that restored the current format. This
|
||
would be used by the debugger, and also could be used by purify to delete parts
|
||
of the debug-info even when the compiler dumps it in crunched form.
|
||
[Note that this isn't terribly important if purify is smart about
|
||
debug-info...]
|
||
|\#
|
||
|
||
|
||
Compiled source map representation:
|
||
|
||
[\#\#\# store in debug-function PC at which env is properly initialized, i.e.
|
||
args (and return-pc, etc.) in internal locations. This is where a
|
||
:function-start breakpoint would break.]
|
||
|
||
[\#\#\# Note that that we can easily cache the form-number => source-path or
|
||
form-number => form translation using a vector indexed by form numbers that we
|
||
build during a walk.]
|
||
|
||
|
||
|
||
|
||
Instead of using source paths in the debug-info, use "form numbers". The form
|
||
number of a form is the number of forms that we walk to reach that form when
|
||
doing a pre-order walk of the source form. [Might want to use a post-order
|
||
walk, as that would more closely approximate evaluation order.]
|
||
|
||
|
||
We probably want to continue using source-paths in the compiler, since they are
|
||
quick to compute and to get you to a particular form. [\#\#\# But actually, I
|
||
guess we don't have to precompute the source paths and annotate nodes with
|
||
them: instead we could annotate the nodes with the actual original source form.
|
||
Then if we wanted to find the location of that form, we could walk the root
|
||
source form, looking that original form. But we might still need to enter all
|
||
the forms in a hashtable so that we can tell during IR1 conversion that a given
|
||
form appeared in the original source.]
|
||
|
||
|
||
Note that form numbers have an interesting property: it is quite efficient to
|
||
determine whether an arbitrary form is a subform of some other form, since the
|
||
form number of B will be > than A's number and < A's next sibling's number iff
|
||
B is a subform of A.
|
||
|
||
This should be quite useful for doing the source=>pc mapping in the debugger,
|
||
since that problem reduces to finding the subset of the known locations that
|
||
are for subforms of the specified form.
|
||
|
||
|
||
Assume a byte vector with a standard variable-length integer format, something
|
||
like this:
|
||
0..253 => the integer
|
||
254 => read next two bytes for integer
|
||
255 => read next four bytes for integer
|
||
|
||
Then a compiled debug block is just a sequence of variable-length integers in a
|
||
particular order, something like this:
|
||
number of successors
|
||
...offsets of each successor in the function's blocks vector...
|
||
first PC
|
||
[offset of first top-level form (in forms) (only if not component default)]
|
||
form number of first source form
|
||
first live mask (length in bytes determined by number of VARIABLES)
|
||
...more <PC, top-level form offset, form-number, live-set> tuples...
|
||
|
||
We determine the number of locations recorded in a block by the finding the
|
||
start of the next compiled debug block in the blocks vector.
|
||
|
||
[\#\#\# Actually, only need 2 bits for number of successors {0,1,2}. We might
|
||
want to use other bits in the first byte to indicate the kind of location.]
|
||
[\#\#\# We could support local packing by having a general concept of "alternate
|
||
locations" instead of just regular and save locations. The location would have
|
||
a bit indicating that there are alternate locations, in which case we read the
|
||
number of alternate locations and then that many more SC-OFFSETs. In the
|
||
debug-block, we would have a second bit mask with bits set for TNs that are in
|
||
an alternate location. We then read a number for each such TN, with the value
|
||
being interpreted as an index into the Location's alternate locations.]
|
||
|
||
|
||
|
||
It looks like using structures for the compiled-location-info is too bulky.
|
||
Instead we need some packed binary representation.
|
||
|
||
First, let's represent a SC/offset pair with an "SC-Offset", which is an
|
||
integer with the SC in the low 5 bits and the offset in the remaining bits:
|
||
----------------------------------------------------
|
||
| Offset (as many bits as necessary) | SC (5 bits) |
|
||
----------------------------------------------------
|
||
Probably the result should be constrained to fit in a fixnum, since it will be
|
||
more efficient and gives more than enough possible offsets.
|
||
|
||
We can the represent a compiled location like this:
|
||
single byte of boolean flags:
|
||
uninterned name
|
||
packaged name
|
||
environment-live
|
||
has distinct save location
|
||
has ID (name not unique in this fun)
|
||
name length in bytes (as var-length integer)
|
||
...name bytes...
|
||
[if packaged, var-length integer that is package name length]
|
||
...package name bytes...]
|
||
[If has ID, ID as var-length integer]
|
||
SC-Offset of primary location (as var-length integer)
|
||
[If has save SC, SC-Offset of save location (as var-length integer)]
|
||
|
||
|
||
|
||
|
||
But for a whizzy breakpoint facility, we would need a good source=>code map.
|
||
Dumping a complete code=>source map might be as good a way as any to represent
|
||
this, due to the one-to-many relationship between source and code locations.
|
||
|
||
We might be able to get away with just storing the source locations for the
|
||
beginnings of blocks and maintaining a mapping from code ranges to blocks.
|
||
This would be fine both for the profiler and for the "where am I running now"
|
||
indication. Users might also be convinced that it was most interesting to
|
||
break at block starts, but I don't really know how easily people could develop
|
||
an understanding of basic blocks.
|
||
|
||
It could also be a bit tricky to map an arbitrary user-designated source
|
||
location to some "closest" source location actually in the debug info.
|
||
This problem probably exists to some degree even with a full source map, since
|
||
some forms will never appear as the source of any node. It seems you might
|
||
have to negotiate with the user. He would mouse something, and then you would
|
||
highlight some source form that has a common prefix (i.e. is a prefix of the
|
||
user path, or vice-versa.) If they aren't happy with the result, they could
|
||
try something else. In some cases, the designated path might be a prefix of
|
||
several paths. This ambiguity might be resolved by picking the shortest path
|
||
or letting the user choose.
|
||
|
||
At the primitive level, I guess what this means is that the structure of source
|
||
locations (i.e. source paths) must be known, and the source=>code operation
|
||
should return a list of <source,code> pairs, rather than just a list of code
|
||
locations. This allows the debugger to resolve the ambiguity however it wants.
|
||
|
||
I guess the formal definition of which source paths we would return is:
|
||
All source paths in the debug info that have a maximal common prefix with
|
||
the specified path. i.e. if several paths have the complete specified path
|
||
as a prefix, we return them all. Otherwise, all paths with an equally
|
||
large common prefix are returned: if the path with the most in common
|
||
matches only the first three elements, then we return all paths that match
|
||
in the first three elements. As a degenerate case (which probably
|
||
shouldn't happen), if there is no path with anything in common, then we
|
||
return *all* of the paths.
|
||
|
||
|
||
|
||
In the DEBUG-SOURCE structure we may ultimately want a vector of the start
|
||
positions of each source form, since that would make it easier for the debugger
|
||
to locate the source. It could just open the file, FILE-POSITION to the form,
|
||
do a READ, then loop down the source path. Of course, it could read each form
|
||
starting from the beginning, but that might be too slow.
|
||
|
||
|
||
Do XEPs really need Debug-Functions? The only time that we will commonly end
|
||
up in the debugger on an XEP is when an argument type check fails. But I
|
||
suppose it would be nice to be able to print the arguments passed...
|
||
|
||
|
||
Note that assembler-level code motion such as pipeline reorganization can cause
|
||
problems with our PC maps. The assembler needs to know that debug info markers
|
||
are different from real labels anyway, so I suppose it could inhibit motion
|
||
across debug markers conditional on policy. It seems unworthwhile to remember
|
||
the node for each individual instruction.
|
||
|
||
|
||
For tracing block-compiled calls:
|
||
Info about return value passing locations?
|
||
Info about where all the returns are?
|
||
|
||
We definitely need the return-value passing locations for debug-return. The
|
||
question is what the interface should be. We don't really want to have a
|
||
visible debug-function-return-locations operation, since there are various
|
||
value passing conventions, and we want to paper over the differences.
|
||
|
||
|
||
Probably should be a compiler option to initialize stack frame to a special
|
||
uninitialized object (some random immediate type). This would aid debugging,
|
||
and would also help GC problems. For the latter reason especially, this should
|
||
be locally-turn-onable (off of policy? the new debug-info quality?).
|
||
|
||
|
||
What about the interface between the evaluator and the debugger? (i.e. what
|
||
happens on an error, etc.) Compiler error handling should be integrated with
|
||
run-time error handling. Ideally the error messages should look the same.
|
||
Practically, in some cases the run-time errors will have less information. But
|
||
the error should look the same to the debugger (or at least similar).
|
||
|
||
|
||
|
||
;;;; Debugger interface:
|
||
|
||
How does the debugger interface to the "evaluator" (where the evaluator means
|
||
all of native code, byte-code and interpreted IR1)? It seems that it would be
|
||
much more straightforward to have a consistent user interface to debugging
|
||
all code representations if there was a uniform debugger interface to the
|
||
underlying stuff, and vice-versa.
|
||
|
||
Of course, some operations might not be supported by some representations, etc.
|
||
For example, fine-control stepping might not be available in native code.
|
||
In other cases, we might reduce an operation to the lowest common denominator,
|
||
for example fetching lexical variables by string and admitting the possibility
|
||
of ambiguous matches. [Actually, it would probably be a good idea to store the
|
||
package if we are going to allow variables to be closed over.]
|
||
|
||
Some objects we would need:
|
||
Location:
|
||
The constant information about the place where a value is stored,
|
||
everything but which particular frame it is in. Operations:
|
||
location name, type, etc.
|
||
location-value frame location (setf'able)
|
||
monitor-location location function
|
||
Function is called whenever location is set with the location,
|
||
frame and old value. If active values aren't supported, then we
|
||
dummy the effect using breakpoints, in which case the change won't
|
||
be noticed until the end of the block (and intermediate changes
|
||
will be lost.)
|
||
debug info:
|
||
All the debug information for a component.
|
||
Frame:
|
||
frame-changed-locations frame => location*
|
||
Return a list of the locations in frame that were changed since the
|
||
last time this function was called. Or something. This is for
|
||
displaying interesting state changes at breakpoints.
|
||
save-frame-state frame => frame-state
|
||
restore-frame-state frame frame-state
|
||
These operations allow the debugger to back up evaluation, modulo
|
||
side-effects and non-local control transfers. This copies and
|
||
restores all variables, temporaries, etc, local to the frame, and
|
||
also the current PC and dynamic environment (current catch, etc.)
|
||
|
||
At the time of the save, the frame must be for the running function
|
||
(not waiting for a call to return.) When we restore, the frame
|
||
becomes current again, effectively exiting from any frames on top.
|
||
(Of course, frame must not already be exited.)
|
||
|
||
Thread:
|
||
Representation of which stack to use, etc.
|
||
Block:
|
||
What successors the block has, what calls there are in the block.
|
||
(Don't need to know where calls are as long as we know called function,
|
||
since can breakpoint at the function.) Whether code in this block is
|
||
wildly out of order due to being the result of loop-invariant
|
||
optimization, etc. Operations:
|
||
block-successors block => code-location*
|
||
block-forms block => (source-location code-location)*
|
||
Return the corresponding source locations and code locations for
|
||
all forms (and form fragments) in the block.
|
||
|
||
|
||
Variable maps:
|
||
|
||
There are about five things that the debugger might want to know about a
|
||
variable:
|
||
|
||
Name
|
||
Although a lexical variable's name is "really" a symbol (package and
|
||
all), in practice it doesn't seem worthwhile to require all the symbols
|
||
for local variable names to be retained. There is much less VM and GC
|
||
overhead for a constant string than for a symbol. (Also it is useful
|
||
to be able to access gensyms in the debugger, even though they are
|
||
theoretically ineffable).
|
||
|
||
ID
|
||
Which variable with the specified name is this? It is possible to have
|
||
multiple variables with the same name in a given function. The ID is
|
||
something that makes Name unique, probably a small integer. When
|
||
variables aren't unique, we could make this be part of the name, e.g.
|
||
"FOO\#1", "FOO\#2". But there are advantages to keeping this separate,
|
||
since in many cases lifetime information can be used to disambiguate,
|
||
making qualification unnecessary.
|
||
|
||
SC
|
||
When unboxed representations are in use, we must have type information
|
||
to properly read and write a location. We only need to know the
|
||
SC for this, which would be amenable to a space-saving
|
||
numeric encoding.
|
||
|
||
Location
|
||
Simple: the offset in SC. [Actually, we need the save location too.]
|
||
|
||
Lifetime
|
||
In what parts of the program does this variable hold a meaningful
|
||
value? It seems prohibitive to record precise lifetime information,
|
||
both in space and compiler effort, so we will have to settle for some
|
||
sort of approximation.
|
||
|
||
The finest granularity at which it is easy to determine liveness is the
|
||
the block: we can regard the variable lifetime as the set of blocks
|
||
that the variable is live in. Of course, the variable may be dead (and
|
||
thus contain meaningless garbage) during arbitrarily large portions of
|
||
the block.
|
||
|
||
Note that this subsumes the notion of which function a variable belongs
|
||
to. A given block is only in one function, so the function is
|
||
implicit.
|
||
|
||
|
||
The variable map should represent this information space-efficiently and with
|
||
adequate computational efficiency.
|
||
|
||
The SC and ID can be represented as small integers. Although the ID can in
|
||
principle be arbitrarily large, it should be <100 in practice. The location
|
||
can be represented by just the offset (a moderately small integer), since the
|
||
SB is implicit in the SC.
|
||
|
||
The lifetime info can be represented either as a bit-vector indexed by block
|
||
numbers, or by a list of block numbers. Which is more compact depends both on
|
||
the size of the component and on the number of blocks the variable is live in.
|
||
In the limit of large component size, the sparse representation will be more
|
||
compact, but it isn't clear where this crossover occurs. Of course, it would
|
||
be possible to use both representations, choosing the more compact one on a
|
||
per-variable basis. Another interesting special case is when the variable is
|
||
live in only one block: this may be common enough to be worth picking off,
|
||
although it is probably rarer for named variables than for TNs in general.
|
||
|
||
If we dump the type, then a normal list-style type descriptor is fine: the
|
||
space overhead is small, since the shareability is high.
|
||
|
||
We could probably save some space by cleverly representing the var-info as
|
||
parallel vectors of different types, but this would be more painful in use.
|
||
It seems better to just use a structure, encoding the unboxed fields in a
|
||
fixnum. This way, we can pass around the structure in the debugger, perhaps
|
||
even exporting it from the the low-level debugger interface.
|
||
|
||
[\#\#\# We need the save location too. This probably means that we need two slots
|
||
of bits, since we need the save offset and save SC. Actually, we could let the
|
||
save SC be implied by the normal SC, since at least currently, we always choose
|
||
the same save SC for a given SC. But even so, we probably can't fit all that
|
||
stuff in one fixnum without squeezing a lot, so we might as well split and
|
||
record both SCs.
|
||
|
||
In a localized packing scheme, we would have to dump a different var-info
|
||
whenever either the main location or the save location changes. As a practical
|
||
matter, the save location is less likely to change than the main location, and
|
||
should never change without the main location changing.
|
||
|
||
One can conceive of localized packing schemes that do saving as a special case
|
||
of localized packing. If we did this, then the concept of a save location
|
||
might be eliminated, but this would require major changes in the IR2
|
||
representation for call and/or lifetime info. Probably we will want saving to
|
||
continue to be somewhat magical.]
|
||
|
||
|
||
How about:
|
||
|
||
(defstruct var-info
|
||
;;
|
||
;; This variable's name. (symbol-name of the symbol)
|
||
(name nil :type simple-string)
|
||
;;
|
||
;; The SC, ID and offset, encoded as bit-fields.
|
||
(bits nil :type fixnum)
|
||
;;
|
||
;; The set of blocks this variable is live in. If a bit-vector, then it has
|
||
;; a 1 when indexed by the number of a block that it is live in. If an
|
||
;; I-vector, then it lists the live block numbers. If a fixnum, then that is
|
||
;; the number of the sole live block.
|
||
(lifetime nil :type (or vector fixnum))
|
||
;;
|
||
;; The variable's type, represented as list-style type descriptor.
|
||
type)
|
||
|
||
Then the debug-info holds a simple-vector of all the var-info structures for
|
||
that component. We might as well make it sorted alphabetically by name, so
|
||
that we can binary-search to find the variable corresponding to a particular
|
||
name.
|
||
|
||
We need to be able to translate PCs to block numbers. This can be done by an
|
||
I-Vector in the component that contains the start location of each block. The
|
||
block number is the index at which we find the correct PC range. This requires
|
||
that we use an emit-order block numbering distinct from the IR2-Block-Number,
|
||
but that isn't any big deal. This seems space-expensive, but it isn't too bad,
|
||
since it would only be a fraction of the code size if the average block length
|
||
is a few words or more.
|
||
|
||
An advantage of our per-block lifetime representation is that it directly
|
||
supports keeping a variable in different locations when in different blocks,
|
||
i.e. multi-location packing. We use a different var-info for each different
|
||
packing, since the SC and offset are potentially different. The Name and ID
|
||
are the same, representing the fact that it is the same variable. It is here
|
||
that the ID is most significant, since the debugger could otherwise make
|
||
same-name variables unique all by itself.
|
||
|
||
|
||
|
||
Stack parsing:
|
||
|
||
[\#\#\# Probably not worth trying to make the stack parseable from the bottom up.
|
||
There are too many complications when we start having variable sized stuff on
|
||
the stack. It seems more profitable to work on making top-down parsing robust.
|
||
Since we are now planning to wire the bottom-up linkage info, scanning from the
|
||
bottom to find the top frame shouldn't be too inefficient, even when there was
|
||
a runaway recursion. If we somehow jump into hyperspace, then the debugger may
|
||
get confused, but we can debug this sort of low-level system lossage using
|
||
ADB.]
|
||
|
||
|
||
There are currently three relevant context pointers:
|
||
-- The PC. The current PC is wired (implicit in the machine). A saved
|
||
PC (RETURN-PC) may be anywhere in the current frame.
|
||
-- The current stack context (CONT). The current CONT is wired. A saved
|
||
CONT (OLD-CONT) may be anywhere in the current frame.
|
||
-- The current code object (ENV). The current ENV is wired. When saved,
|
||
this is extra-difficult to locate, since it is saved by the caller, and is
|
||
thus at an unknown offset in OLD-CONT, rather than anywhere in the current
|
||
frame.
|
||
|
||
We must have all of these to parse the stack.
|
||
|
||
With the proposed Debug-Function, we parse the stack (starting at the top) like
|
||
this:
|
||
1] Use ENV to locate the current Debug-Info
|
||
2] Use the Debug-Info and PC to determine the current Debug-Function.
|
||
3] Use the Debug-Function to find the OLD-CONT and RETURN-PC.
|
||
4] Find the old ENV by searching up the stack for a saved code object
|
||
containing the RETURN-PC.
|
||
5] Assign old ENV to ENV, OLD-CONT to CONT, RETURN-PC to PC and goto 1.
|
||
|
||
If we changed the function representation so that the code and environment were
|
||
a single object, then the location of the old ENV would be simplified. But we
|
||
still need to represent ENV as separate from PC, since interrupts and errors
|
||
can happen when the current PC isn't positioned at a valid return PC.
|
||
|
||
It seems like it might be a good idea to save OLD-CONT, RETURN-PC and ENV at
|
||
the beginning of the frame (before any stack arguments). Then we wouldn't have
|
||
to search to locate ENV, and we also have a hope of parsing the stack even if
|
||
it is damaged. As long as we can locate the start of some frame, we can trace
|
||
the stack above that frame. We can recognize a probable frame start by
|
||
scanning the stack for a code object (presumably a saved ENV).
|
||
|
||
Probably we want some fairly general
|
||
mechanism for specifying that a TN should be considered to be live for the
|
||
duration of a specified environment. It would be somewhat easier to specify
|
||
that the TN is live for all time, but this would become very space-inefficient
|
||
in large block compilations.
|
||
|
||
This mechanism could be quite useful for other debugger-related things. For
|
||
example, when debuggability is important, we could make the TNs holding
|
||
arguments live for the entire environment. This would guarantee that a
|
||
backtrace would always get the right value (modulo setqs).
|
||
|
||
Note that in this context, "environment" means the Environment structure (one
|
||
per non-let function). At least according to current plans, even when we do
|
||
inter-routine register allocation, the different functions will have different
|
||
environments: we just "equate" the environments. So the number of live
|
||
per-environment TNs is bounded by the size of a "function", and doesn't blow up
|
||
in block compilation.
|
||
|
||
The implementation is simple: per-environment TNs are flagged by the
|
||
:Environment kind. :Environment TNs are treated the same as :Normal TNs by
|
||
everyone except for lifetime/conflict analysis. An environment's TNs are also
|
||
stashed in a list in the IR2-Environment structure. During during the conflict
|
||
analysis post-pass, we look at each block's environment, and make all the
|
||
environment's TNs always-live in that block.
|
||
|
||
We can implement the "fixed save location" concept needed for lazy frame
|
||
creation by allocating the save TNs as wired TNs at IR2 conversion time. We
|
||
would use the new "environment lifetime" concept to specify the lifetimes of
|
||
the save locations. There isn't any run-time overhead if we never get around
|
||
to using the save TNs. [Pack would also have to notice TNs with pre-allocated
|
||
save TNs, packing the original TN in the stack location if its FSC is the
|
||
stack.]
|
||
|
||
|
||
We want a standard (recognizable) format for an "escape" frame. We must make
|
||
an escape frame whenever we start running another function without the current
|
||
function getting a chance to save its registers. This may be due either to a
|
||
truly asynchronous event such as a software interrupt, or due to an "escape"
|
||
from a miscop. An escape frame marks a brief conversion to a callee-saves
|
||
convention.
|
||
|
||
Whenever a miscop saves registers, it should make an escape frame. This
|
||
ensures that the "current" register contents can always be located by the
|
||
debugger. In this case, it may be desirable to be able to indicate that only
|
||
partial saving has been done. For example, we don't want to have to save all
|
||
the FP registers just so that we can use a couple extra general registers.
|
||
|
||
When when the debugger see an escape frame, it knows that register values are
|
||
located in the escape frame's "register save" area, rather than in the normal
|
||
save locations.
|
||
|
||
It would be nice if there was a better solution to this internal error concept.
|
||
One problem is that it seems there is a substantial space penalty for emitting
|
||
all that error code, especially now that we don't share error code between
|
||
errors because we want to preserve the source context in the PC. But this
|
||
probably isn't really all that bad when considered as a fraction of the code.
|
||
For example, the check part of a type check is 12 bytes, whereas the error part
|
||
is usually only 6. In this case, we could never reduce the space overhead for
|
||
type checks by more than 1/3, thus the total code size reduction would be
|
||
small. This will be made even less important when we do type check
|
||
optimizations to reduce the number of type checks.
|
||
|
||
Probably we should stick to the same general internal error mechanism, but make
|
||
it interact with the debugger better by allocating linkage registers and
|
||
allowing proceedable errors. We could support shared error calls and
|
||
non-proceedable errors when space is more important than debuggability, but
|
||
this is probably more complexity than is worthwhile.
|
||
|
||
We jump or trap to a routine that saves the context (allocating at most the
|
||
return PC register). We then encode the error and context in the code
|
||
immediately following the jump/trap. (On the MIPS, the error code can be
|
||
encoded in the trap itself.) The error arguments would be encoded as
|
||
SC-offsets relative to the saved context. This could solve both the
|
||
arg-trashing problem and save space, since we could encode the SC-offsets more
|
||
tersely than the corresponding move instructions.
|