For example, I've been working on a big integer library. Addition is a bit annoying, because you need to add 3 numbers (the prior carry bit, and the two digits you're adding) and get out a new carry bit and a resulting digit. This is a bit cumbersome:
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.I assume you've been using Compiler Explorer a lot?
Defined, by default checked in debug mode and unchecked in release, unsigned wraps and signed wraps as two's complement. This can be overridden by explicitly setting overflow-checks in the relevant profile.
It also, separately, has explicitly wrapping, saturating and checking versions of basic arithmetic operations.
I've found it very difficult to provoke rustc into emitting an ADC: the only case where it does so AFAICT is when adding u128s, which are implemented using u64s. Not sure why, except that the shortest function I could think of to emulate ADC is kind of baroque, and it's possible the compiler can't figure it out.
I've mostly been using cpuprofiler[1] and Vtune to simultaneously profile my code and show the assembly. In theory they both provide timing information per-instruction, but I don't really trust it. For the 6 adc instructions above, it shows the number of clock ticks as ranging from 22 million to 3 billion, which doesn't make sense to me. But at least it shows me the assembly!
[1] https://docs.rs/cpuprofiler/0.0.4/cpuprofiler/index.html
https://news.ycombinator.com/item?id=23351007
specifically recommends against the carry variants of addition, because the instructions are still dependent on each other and don't pipeline well. In other words, it's using the same algorithm, just buried in a single instruction, and that doesn't necessarily make it faster.
Have you considered using a strategy similar to what the article suggests? I think the HN comments also had some additional suggestions.
The approach in the article only works if you are adding a lot of numbers together, and then indeed doing carry propagation once at the end is obviously faster.
But of course there is no way that doing the carry propagation yourself on one addition can possibly be faster on a decent CPU that implements add-with-carry efficienly.
That certainly sounds useful for, e.g., totaling an array of big integers.
Did you miss the "And got a 3x speedup" part of the post you were replying to? Actual benchmarks of real code always trump theoretical deliberations.
A big integer library may have many use cases; a benchmark only shows one data point. It's possible that by deferring carry work across more operations he'd see an even bigger improvement.
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...
Just within the rust world, https://www.nickwilcox.com/blog/autovec2/ from a few weeks ago talks about the limitations with the compiler around vectorisation, demonstrating that with hand-rolled assembly they were able to cut the runtime in half, vs what the compiler was able to produce.
Outside of the rust world, you'll often see hand-rolled assembly in performance critical code across a spectrum of uses, e.g. OpenSSL uses it extensively for performance reasons, Go has it in a number of places in their libraries, with a pure go fall-back, and so on.
edit: Thinking about it, that's not true. I was dealing with some hot code running on some MIPS architecture stuff a while back and the compiler's generated ASM was merely "okay", for the standard library. Alternative libraries with hand-rolled ASM ran significantly faster and we were going to leverage those, when another team came up with architectural changes that allowed us to take that away from the MIPS part of the stack.
Also people doing cryptography things occasionally need access to specific ASM instructions for their work. I believe AES has specialized instructions, but there are more cases.
You're not wrong that writing inline ASM is a fairly specialized need, but for those who require it this will come in incredibly useful.
[1]: I still need to try using this, so I might be wrong. I was waiting on inline assembly to be available before trying tho, so quite excited it's now available.
https://doc.rust-lang.org/core/intrinsics/fn.ctpop.html
https://doc.rust-lang.org/std/?search=count_ones
There are still use cases for raw assembly untouched by the optimizer - e.g. constant time cryptography functions designed to thwart timing attacks might desire control over the exact instruction stream. Although, most of that can be tackled with out-of-line assembly - as one must do with x64 MSVC for example.
There are literally tens of thousands of bugs in the GCC and LLVM/Clang bug trackers about that.
I don't remember the last time I read some compiler-generated asm and thought "wow, that's great code".
Compilers almost always write better code than most humans.
Compilers are programmed by humans. You are claiming that somehow compilers have learned to produce better assembly than the humans that programmed them.
I am a compiler writer, and know many compiler writers. I have yet to find one that would stand by your claim. When I look at the output of LLVM or GCC, I am ashamed.
I wish I had more time to make LLVM much better, but actually, 99% of my time is spent in fixing bugs filled by users that are neither compiler writers nor assembly programmers, and they too realized how bad the machine code emitted by these tools is.
So sure, a compiler emits better assembly than a human that hasn't learn assembly programming. It also emits better assembly than all humans that do not know how to program.
But no, it does not produce _by far_ better assembly than any programmer that actually cares about machine code enough to just take a look at it and check it.