x86-64 machine code, 24 bytes
Or 1 less for 32-bit mode, with 1-byte dec ecx
. If anyone has any other ideas for golfing further, please comment; I expect there's room for improvement. A straightforward binary exponentiation that doesn't have to produce the exact same proof-of-"work" array output is only 19 bytes.
Actually implements a binary-exponentiation algorithm, not doing a separate pow
for every output element. Only one or two other answers do this, and I think this is the only iterative (non-recursive) one of those.
This is a function that takes an output pointer in RDI, to an array of DWORDs. x
in ESI, n
in EBX. (EDX is clobbered by mul
. This can be signed if you want; mul
and imul
are the same binary operation on EAX, differing only in the EDX high half. They're the same size.)
Outputs: Array of steps in the memory the caller passed, RDI points to one-past-end. EAX = x**n
.
trimmed NASM listing: address, machine-code (the answer), source
expint: ; uint32_t *out (rdi), x = esi n = ebx
00 6A01 push 1
02 58 pop rax ; mov eax,1
03 AB stosd ; *rdi++ = eax
04 0FBDCB bsr ecx, ebx ; bit-scan-reverse. we need to loop bits high to low; a set bit cares about (gets squared by) how many bits are *below* it, not above.
; ECX = bit-index of highest set bit
07 740E jz .n0 ; exponent=0 is a special case
.loop:
09 F7E0 mul eax ; square total
0B 0FB3CB btr ebx, ecx ; bit-test-reset. Or just BT, clearing n as we go wasn't helpful.
0E 7302 jnc .was_even
10 F7E6 mul esi ; total *= x; (destroys EDX with the high-half result, but this saves a byte vs. imul eax, esi)
.was_even:
12 AB stosd
13 FFC9 dec ecx
15 7DF2 jge .loop ; }while(--bitpos >= 0); // loop runs for ECX=0, can't use LOOP unless we left-shifted EBX or something
.n0:
17 C3 ret
size: 0x18 = 24 bytes
It feels like there should be room to golf more, but it's tricky to cover the corner cases (like n==0
is costing 2 bytes) and the off-by-ones (e.g. having to run the loop body for ECX=0 means I can't use loop
).
Starting with 1
isn't as much of a burden as I'd thought it might be, mostly just requiring an extra stosd
to output before the first multiply and after the last. We need to start with the MSB, not LSB, so we always have a 1
bit so the first iteration can do 1*1 * x
to get us started with total=x. That does let us fall into the loop with total=1
after all. If not for having to store the n
, though, I'd likely start with EAX=x
and adjust the loop somehow.
(The test-case I asked for with a non-trivial even power turned out not to be relevant for this; I hadn't realized you needed to start from the high end for an iterative version. But it makes sense if you think about it: a 1
in the lowest bit of n
results in a *x
at the end. But if you do that at the start, a bunch of squaring happens so that odd power becomes an even power. If you just shr ebx, 1
/ jnc
to divide by 2 and check for odd/even, you'd effectively be bit-reversing the exponent, or something like that. Which works for exponents like 15
that have no significant zeros, but not others.)
Update: you don't have to start from the MSB, I just had a brain fart: the normal algorithm would do x *= x;
every iteration, only multiplying it into the total for set bits in n
. The algorithm is identical to multiplication by shifting-and-adding, in case it helps to think about how adding partial products works. (Stepanov's lectures on algorithms uses binary exponentiation as one of the main examples, pointing out that replacing +
with *
makes exponentiation instead of multiplication.) This question's presentation in terms of square-rooting the total in reverse got me stuck on squaring the total instead of x, and if you do that then you have to go from MSB downward.
No CPU I'm aware of has an integer sqrt instruction, or a single-instruction pow
to give a starting point. (x87 has some exponential stuff, but takes multiple instructions to do pow. Still maybe worth considering.
Hopefully we can still recover all the same intermediate values this question wants, while doing shr ecx,1
/ jnc
. But that would make starting with 1
inconvenient.
BMI2 mulx
is a 5-byte instruction, so even if we could reuse FLAGS to save a dec or test, it wouldn't pay for itself.
Try it online! with _start
test caller (doesn't print: use GDB display *(int[8]*)($rsp+8)
to display the output array state as you single-step, given that the caller passes RDI=RSP as the output.)
19 bytes, different array from a more normal algorithm:
expint_v2: ; uint32_t *out (rdi), x = esi n = ebx
push 1
pop rax ; mov eax,1
stosd
.loop: ;do{
shr ebx, 1 ; n /= 2, CF = old low bit
jnc .was_even
mul esi ; total *= x; (destroys EDX with the high-half result, but this saves a byte vs. imul eax, esi)
.was_even:
stosd ; *out++ = eax
imul esi, esi ; square x
test ebx, ebx
jnz .loop ;}while(n!=0);
ret
Same inputs as before, same result in EAX and the highest array element (since the last bit shifted out before it becomes zero is a 1
, unless n=0
in which case we leave the loop without having done mul esi
.)
e.g. for 3^21 it produces these steps, where 1870418611
is 3^21 mod 2^32.
{1, 3, 3, 243, 243, 1870418611}
The x*=x
steps where n
doesn't have a set bit don't change the running product. (Like for multiply by addition, where you're not adding a partial product at that shift.)
I don't think it's possible to recover the same proof-of-work temporaries from this (even with a mov
/imul
into another temp), because we're working from the low bit up, not from the high bit down.
Starting with 1
is actually helpful, since we actually need to not start with total = x
if n
is even, but after we square x this iteration we no longer have any odd power of the original x
left to multiply by, so the total would remain an odd power.
Starting with total=1
also makes the n=0 case Just Work for free.
Anyway, this order of temporaries is perhaps less interesting, but could probably also be recovered from a recursive implementation. I'm curious whether another challenge, perhaps titled iterative binary exponentiation, would get different answers in other languages. Probably still easier to use **
or ^
built-ins in languages that have them, after generating the right array of what, prefix-sum of bit-indices of non-zero bits or something?