4 ms·
> I find it funny and think it's pretty telling that there are bold claims made that are only crossed out after the author is corrected crossed out? have you s
by demarq 10y ago
> I find it funny and think it's pretty telling that there are bold claims made that are only crossed out after the author is corrected
crossed out? have you seen the follow up post... http://robert.ocallahan.org/2017/04/rust-optimizations-that-c-cant-do_5.html http://robert.ocallahan.org/2017/04/rust-optimizations-that-...
The error was in his example not his assertion.
- MaulingMonkey 10y ago> have you seen the follow up post... Yes. By adding a single __restrict__ (between the ampersand and the v) I can make the inner loop of the C++: .LBB0_1: # =>This Inner Loop Header: Depth=1 call rbx dec ebp jne .LBB0_1 Which has hoisted the sum out of the loop, which is even better than the Rust version which wasn't smart enough to turn the add into an out-of-loop imul, making the Rust version two instructions longer: .LBB0_1: inc ebx call r14 add r12, r15 cmp ebx, 100 jl .LBB0_1 This makes the author technically right: restrict is only standard C99, and a compiler extension in C++. EDIT: I might as well share the godbolt link: https://godbolt.org/g/jFmiRV https://godbolt.org/g/jFmiRV EDIT x2: And of course one can cheat and simply pass by value like a reasonable person. Ahh, the pitfalls of microbenchmarking... EDIT x3: Also, unless I've misunderstood modern processor architecture, with register renaming and physical registers potentially (often? usually?) outnumbering architectural registers on modern x86 processors, the initial "bad" C++: .LBB0_1: # =>This Inner Loop Header: Depth=1 add rbx, qword ptr [r15] call r14 dec ebp jne .LBB0_1 May still be using a physical register for [r15], despite the slow-looking dereference. If it isn't, it's likely because the callback was complex enough to require so many registers that the callback would be saving out this loop's registers and restoring them anyways. In that context, the pre-restrict C++ could actually be more efficient (4 instructions to Rust's 5, 4 persisted architectural registers[1] to Rust's 4, ditto for physical register requirements...?) Now I'm curious what the best approach to "defeating" the processor's register renaming hardware to force N registers of the inner loop out of physical registers (via increasing the callback's complexity) and what the actual performance impact is on these dueling disassembly snippets. The post-restrict C++ with the hoisted imul, of course, is a clear winner at 3 instructions, and only 2 persisted architectural registers... ([1] ebx, r12, r14, r15 - not counting e.g. the flags the jumps are conditional on, which would not need persisting for any of the snippets)
- MaulingMonkey 10y agoI did try benchmarking the author's two loops (e.g. ignoring my __restrict__ version) at 100m iterations: https://gist.github.com/MaulingMonkey/92f856664094d54bd84a2583105190bb https://gist.github.com/MaulingMonkey/92f856664094d54bd84a25... My i7-5930K @ 3.50 GHz chews through 100m iterations in about: 170±5ms for the rust-style loop 140±5ms for the c++-style loop So ~1.7ns versus ~1.4ns per iteration? I'm not 100% confident in my profiling results in that when culled down to just two calls, I've sometimes seen both benchmarks weigh in roughly the same at ~140kus for the 100m loop. Why the rust version ends up faster in this specific circumstance is beyond me (bursting before thermal cutoff?) This may be down to CPU power states, which I'm failing to control here? That said, anything with a loop seems to consistently favor the C++ loop here (including comparing "cpp" only and "rust" only versions of the benchmarking snippet). Benchmarking disassembly looks equivalent. Pinning to a core doesn't significantly alter my results (keeping one core pegged at 100% this way does slightly improve my results by a few ms - presumably evicting any background system processes from that core?) TL;DR: What rust did in the follow up post also wasn't actually an optimization, in at least one microbenchmark. Interesting! EDIT: A key reminder: When optimizing or investigating performance, always be profiling. If you're optimizing without profiling, you're not optimizing! (The minor exaggerations of this paragraph have not been optimized for effectiveness, I lack perf data...)
- dbaupp 10y ago> TL;DR: What rust did in the follow up post also wasn't actually an optimization, in at least one microbenchmark. Interesting! This isn't a correct conclusion! There's more differences between the code that just the optimisation enabled by the aliasing guarantees: hoisting the load outside the loop. The difference is likely to be the choice of the Rust compiler to use inc + cmp, instead of just dec like the C++ one does, which should be independent of the ability to hoist the load (indeed, if I change the Rust to use a while loop and manual incrementing, it results in using dec, and also hoisting the addition outside the loop to a multiplication, and similarly, if the 0..100 is changed to 0_i64..100, i.e. using 64-bit integers, the same hoisting happens... this looks like another case of LLVM not fully untangling Option<i32>). If one was wanting to compare the actual quality of the generated code between two specific compilers, sure, but the intention is presumably to compare the actual "interesting" optimisation enabled by the language semantics. Your conclusion would be correct if you got the same results after controlling for the other variables, i.e. make the C++ use inc & cmp, or the Rust use dec.