How Rust 1.26 more than tripled the speed of my code
troubles.md
troubles.md
The disassembly for the Rust output of `u256_full_mul` at the end is also terrible, and it looks like the output of a debug build. There's no reason an optimizing compiler should be outputting `pushfd` and `popfd` in performant code. The benchmarked loop does not have those instructions.
I've been wondering about how much instructions cost lately and would like to be able to know this without the need of asking others.
I won't speak to the quality of the generated code otherwise, but this bit of evidence by itself doesn't seem persuasive.
(edit: pbsd is right: per Agner Fog, these guys are slow. It's likely that the L/SAHF trick is the one I was remembering.)
You could probably achieve the same result (assuming you want to avoid adc instructions, for some reason) by using setc r8 plus shr r8, 1 to get the carry back.
https://play.rust-lang.org/?version=nightly&mode=release
However, Clang appears to do the right thing with equivalent C code:
This is strange because Rust and Clang both use LLVM for their backend.
Looking at the LLVM IR produced, Rust's IR straightforwardly corresponds to the source code: a load instruction (to evaluate `self_t[0]`) whose result is passed to an asm instruction:
%0 = getelementptr inbounds %U256, %U256* %self, i64 0, i32 0, i64 0
%1 = load i64, i64* %0, align 8
tail call void asm sideeffect "mulq $0", "m,~{dirflag},~{fpsr},~{flags}"(i64 %1) #1, !srcloc !0
But Clang's omits the load, and transforms the "m" constraint to "* m" (without the space, bah HN formatting): %2 = getelementptr inbounds %struct.U256, %struct.U256* %0, i64 0, i32 0, i64 0, !dbg !24
call void asm sideeffect "mulq $0", "*m,~{dirflag},~{fpsr},~{flags}"(i64* nonnull %2) #2, !dbg !26, !srcloc !27
Hmm… seems to be related to this Rust bug: https://github.com/rust-lang/rust/issues/16383He/she might be cool, but I think someone familiar with assembly optimisation would think to look at the generated assembly as well, after noticing the speedup is not what you expect. Don't go hand-writing assembly routines thinking your code is fast without actually checking whether your code is fast. :)
Also I would hazard a guess that pushing flag state into a register is a lot faster than the stack.
Finally, it'd be interesting to see the original hand-rolled inline assembly used with "r" instead of "m" to see how llvm stacks up against the intended version without the redundant memory load/stores.
So article is about a flew in previous versions of Rust not supporting 128-bit numbers on x86_64. But title makes it sound like "switching to Rust was the breakthrough". Or am i paranoid about it..
Since titles keep changing on HN (for good reasons), I can’t be sure you saw the same title that I did, but if you did I disagree.
The current title is “How Rust 1.26 more than tripled the speed of my code”.
Because that title specifies a specific version number, I expected the blog post to be about exactly the kind of thing it was; some new feature or improvement in that version of Rust which would make their software run faster compared to the previous version of Rust they were using.
[ed: ok, from a skim, I see this is actually about 256bit multiplication - which makes me curious how just using bigints and * (mul operator) would work in Julia.
Also, I don't get this:
> u256_mul multiplies two 256-bit numbers to get a 256-bit result (in Rust, we just create a 512-bit result and then throw away the top half but in assembly we have a seperate implementation)
How do they know the result will fit in 256 bits? Sounds like they know more about the arguments than both having to be 256 bits long?
Or do they want multiply and shift?]
Here’s an article explaining how to use new instructions to implement large integers multiplication: http://www.intel.com/content/dam/www/public/us/en/documents/...
https://gist.github.com/Vurich/5cb83c773e90fc7a463ccb58e1dad...
Is that right? Wouldn’t that answer look like nonsense? I would have guessed it tossed the lower 64bits so it looked like rounding by truncation at least.
You talk about rounding but if the number's too big then then you're only ever going to round to the max number you could use, so we might as well call that saturation.
Discarding the upper bits means you are implementing modular arithmetic.
Both saturation and modular arithmetic are well defined, and can both be reasonable choices. I think most languages implement modular arithmetic, so that's what most programmers expect, and it seems a reasonable choice to me compared to saturation.
For signed integers, overflow is undefined in C/C++, but generally results in wrapping as well.
Unsigned and signed overflow in rust are both well defined to do one of two things, panic or wrap. If debug assertions are enabled they are defined to panic. In practice if debug assertions are not enabled they will wrap, but I believe that is technically subject to change (into a panic). It will realistically only change if the hardware gets much better at catching overflows quickly.
A panic is usually like a exception that you are really not expected to ever catch except at thread boundaries. You can turn them into a immediate process exit with a compiler flag.
Also at FFI boundaries. (i.e. a panicky Rust function, supplied to foreign code as a callback)
Yes, that's correct.
At least, keeping the lower 64 bits is the usual overflow behavior and if the result of the multiplication fits in 64 bits it just works fine.
For example, many language runtime libraries have pseudo random number generators (PRNGs) use a 32-bit linear congruential generator (LCG). Suppose you want to create a rand(n) function which returns an integer between 0 and n-1. One good way of doing this is multiplying the lcg_output (a 32-bit unsigned integer) by n, and returning the high 32 bits.
It's better to do this than a modulus operation because the low bits on LCG sequences are highly predictable.
(This is how the Delphi Random(n) function works.)
https://doc.rust-lang.org/std/primitive.u64.html#method.chec...
On, say, ARM64, there are two separate instructions: MUL is the normal one that takes two 64-bit inputs and produces one 64-bit output (the lower half of the product), while UMULH ("Unsigned Multiply High") performs the same multiplication but gives you only the upper half. This way, in the common case where you don't care about the upper half, the CPU doesn't have to waste time calculating it.
The moderation team here tacitly encourages it by catering to it. I've seen some threads go through 2 or 3 title changes as people find something new to complain about.
Really it's almost a form of editorializing in itself - the title of the article is what it is, people here just think they know better than the author.