(0) Let me call these "tree of computation" instructions. I don't really
want to say "CISCy" instructions, since, although complex, they often
could be done by combinatoric logic. Or by simple state machines, like
a divider. Less commonly do people stick references to memory, that
might take cache misses, inside.
A fellow I know who has proposed such an ISA calls them "logic cone"
instructions.
(1) First you create "tree of computation" instructions. And publish
them as an ISA.
(2) Then you hide the tree of computation instructions behind a simpler
ISA, and have a compiler or JITter create the "tree of computaion
instructions" in a hidden, non-public, ISA.
(3) Then you have hardware that can dynamically pack simple RISCy
instructions into such "tree of computati8on" microinstructions.
Dynamically, on the fly, via renamer tricks.
Possibly, probably, caching in a decoded instruction cache.
---
However, in this case the hardware solution may have come along before
the explicit ISA solution.
---
My own work on "dynamic instruction rewriting" is a very simple form of
(3). In that work I was mainly limited by the problem that on x86
memory stores and loads tend to interrupt the computation trees or
chains. I am pretty sure that memory renaming or registerization would
help, but at UWisc I decided that was beyond my scope.
By the way: although some of the memory store/loads were artifacts of
x86 with not enough registers, by no means all were. I tended to
concentrate on the SPEC benchmark gcc for my work, and many of the
store/load chains were writing something into an object's data members,
for later retrieval in completely separate methods and functions. Which
means that a compiler might NOT be able to eliminate these, unless it
had really good pointer disambiguation. Whereas a dynamic approach -
hardware or JIT - should be able to.
Now, there are many workloads that are not so pointer intensive. I
often talk about the ratio of computation to pointers. Gcc has a fairly
low ratio. Graphics is often pretty high. I am told that CAD tools are
also pretty high.
It might be nice to have a slightly better ratio than just the ratio of
computation to pointers. Because that might have the same value if you
had 100 independent register to register instructions per pointer chase,
as if you had 100 dependent register to register instructions per
pointer chase. The former is VLIW, that latter is tree of computation.
But I haven't got a simple metric.
In any case, there may be workloads that are even now susceptible to
"tree of computation".
---
I have spent a lot of time thinking about what particuilar arrangements
of ALUs and other execution units might be most generically useful.
Mark Thorson's example, A = BC + CD, is really just a general case of an
inner product D = sum(A[i]*B[i],i=0,n-1). It's SIMD packed vectors if
they are in a vector register, sorting else if the A[i] and B[i[ are in
separate scalar registers.
Anyway, I'll stipulate to any sort of vector reduction and vector
recurrence being a candidate. Many are mentioned on my wiki (although I
probably should have a page summarizing).
To go further, let's restrict ourselves to scalar trees of computation.
Since one can always do such operations for wevery element of vectors,
and then reduce, or do such computation in a recurrence.
How about reviving the DG Nova style stuff, most recently seen in the ARM?:
Consider a basic op like A*B+C (or A*B+C*D).
Make available a generic set of operations on the inputs and outputs:
transform( transform(A) * transform(B) + transform(C) )
where the transformations can be
transform( x )
:: x
:: -x
:: abs(x)
:: -abs(x)
:: shift( x << n )
:: shift( x >> n )
:: extract_field( x, frombit:tobit )
and so on. It's inconvenient to note, but the +/-/abs/nabs stuff might
be available after the shifts and extracts.
These are all relatively simple operations, that do not add too much
delay, and which are used in a surprisingly large number of situations.
(Note: when I say "too much delay", I am thinking not in terms of the
speed demons of the past like Wmt, 8 gate delays per clock, but in terms
of systems where you want to have as many logic levels as possible in a
clock, that can be useful, because that improves yields. (Or even,
asynch logic... nah, don't go there.))
I might also want to have operations such as max(x1,const), max(x1,x2),
min, limit(x1,lwb,upb), and so on. Probably you would mainly want
such limits on the output, but I wonder about having them on inputs.
Truncations and conversions int/float, and between various precisions.
I would also want such a "tree of computation" to not necessarily be
limited to a single result. If it it had multiple results from
independent subtrees, that would just be VLIW - but I think that we
might want to be able to share subtrtees in the same instruction leading
to different results. And/or record (output to a result register)
various sub-positions in the same tree.