# entering factorial: argument n is b0
# drop initial accumulator to belt; bail early if n==0
con(1), cmpeq(b1, 0), brtrue(end);
# now, b0 == 1, b1 == n
loop:
# multiply accumulator by n, then decrement n, then check if n stil not 0
mul(b0, b1), sub(b1, 1), cmpeq(b1, 0), brfalse(loop);
end:
# no matter what, the most recent accumulator is b0
ret(b0);F("fact") %0; sub(b0 %0, 1) %1;
gtrsb(b0 %1, 1) %2,
retnfl(b2 %0),
inner("fact$1_1", b1 %1, b2 %0);
L("fact$1_1") %3 %4; mul(b1 %3, b0 %4) %5;
sub(b0 %4, 1) %6;
gtrsb(b0 %6, 1) %7,
retnfl(b1 %5),
br("fact$1_1", b2 %6, b1 %5);
Note that integer multiply is specified as 3 cycles in this model, so the recurrence in the loop takes three cycles. Other ops are one cycle. Each semicolon is an instruction (and cycle) boundary, ops separated by commas issue and execute together.This tread for example, talks about instruction density of Mill programs. Mill does somewhat poorly, with a pre-alpha quality compiler doing the codegen; https://groups.google.com/forum/#!topic/comp.arch/RY3Bk7O61u...
Comparing several ISAs on the simple program in the first post, a Mill Gold CPU binary weighs in at 337 bytes (and 33 instructions), compared to:
* powerpc: 212 bytes, 53 instructions * aarch64: 204 bytes, 51 instructions * arm: 176 bytes, 44 instructions * x86_64: 135 bytes, 49 instructions * i386: 130 bytes, 55 instructions * thumb2: 120 bytes, 51 instructions * thumb: 112 bytes, 56 instructions
label $0:
%1 = sub(%0, 1) ^0;
%2 = gtrsb(%1, 1) ^1;
br(%2, $1, $2);
label $1 dominators($0) predecessors($0, $1): // loop=$1 header %3 = phi(w, %5, $1, %0, $0);
%4 = phi(w, %6, $1, %1, $0);
%5 = mul(%3, %4) ^2;
%6 = sub(%4, 1) ^3;
%7 = gtrsb(%6, 1) ^4;
br(%7, $1, $2);
label $2 dominators($0) predecessors($0, $1): %8 = phi(w, %0, $0, %5, $1);
retn(%8) ^5;
};Higher-end Mill models have more and bigger things to encode, and so a given program will occupy more bytes than it will on a lower-end member. Thus a belt reference on a Gold is 6 bits, but only 3 on a Tin.
The layer that humans and HLL compiler developers end up with at the lowest seems to resemble a dataflow graph, and the system-specific code converts that into the parallel binary code stream.
The notion of the "belt" helps arrange how long data sticks around in live outputs & buffers, but isn't a physical element in the chip.
load *src, bv
eql <load>, 0
smearx <eql>
pick <smearx0>,None,<load>
store *dest, <pick>
brfl smearx1, loop
This "vectorizes" a plain while-loop by loading as "byte vector". The size of the vector depends on the exact Mill family member implementation's choices, but is at least 8 according to the lecture. The lack of actual register names is of course a loud signal that the Mill is different.Pretty cool, and I feel like I learned something. Thanks!
If you also look a bit further into the talk they show the 'remaining' instruction, which allows you to do this same thing with fixed-count loops.
They typically start with ("Mill team"), and then the rest sounds like it came from the future. It's all about getting the flux capacitors charged.
Can anyone post some low-level code (in assembly, or the closest equivalent if that term is no longer relevant with Mill) implementing some well-known simple algorithm on the Mill? Just to quickly get a feel of what it's all about, how it looks.
Something like computing n! for word-sized integers, or strcmp(), or whatever.
Does anyone have a quick link to something like that?