let (res, carry1) = target_digit.overflowing_add(carry as u64);
let (res, carry2) = res.overflowing_add(other_digit);
*target_digit = res;
carry = carry1 || carry2;
The resulting assembly is a fairly literal translation. We perform the addition using an add instruction, and extract the carry flag into a register using setb. addq (%rdi,%rsi,8), %rcx
setb %r10b
addq (%rdx,%rsi,8), %rcx
setb %al
movq %rcx, (%rdi,%rsi,8)
orb %r10b, %al
movzbl %al, %eax
But there's a dedicated instruction for this, adc. adc adds two operands and the carry flag, while itself setting a carry flag. Manually unrolling the loop a bit, I wrote this assembly: shlb $8, {carry}
adcq 0x00({y0}), {x0}
adcq 0x08({y0}), {x1}
adcq 0x10({y0}), {x2}
adcq 0x18({y0}), {x3}
adcq 0x20({y0}), {x4}
adcq 0x28({y0}), {x5}
setb {carry}
And got a 3x speedup.
Multiplication initially seemed like a very promising use case, since it's basically repeated addition. But I'm not super optimistic about that, because I think it's dominated by the alternate optimization of noticing that the product of two 64 bit numbers cannot saturate the high 64 bits of the resulting 128 bit number, which causes carries to be bounded [1].
[1] https://github.com/rocurley/bignum/blob/b45448a156fb9100ab06...