10 ms·
Comparing Pythagorean triples in C++, D, and Rust
- steveklabnik 8y agoThe reddit thread already has one good relevant comment https://www.reddit.com/r/rust/comments/ab7hsi/comparing_pythagorean_triples_in_c_d_and_rust/ https://www.reddit.com/r/rust/comments/ab7hsi/comparing_pyth...
- stabbles 8y agoWell, that seems more like an issue with the compiler then, cause avoiding overflows would not result in a 2x slow-down in C or C++.
- steveklabnik 8y agoPossibly! Remember that the Rust language and C/C++ have different semantics with regards to overflow. It may be the programmer's "fault" for not writing the code with the equivalent semantics (you can make them have the same semantics, but it's not the default).
- stabbles 8y agoLooking at the Rust source [1] it seems there the `next()` call has some overhead, so it's probably not the compiler. I remember this was simple to implement in Julia [2] where you can iterate over the full range `for i = typemin(Int):typemax(Int) ... end` without overflow and without overhead. Same should be possible in Rust I think. [1] https://doc.rust-lang.org/src/core/iter/range.rs.html#340-353 https://doc.rust-lang.org/src/core/iter/range.rs.html#340-35... [2] https://github.com/JuliaLang/julia/blob/master/base/range.jl#L591-L598 https://github.com/JuliaLang/julia/blob/master/base/range.jl...
- orf 8y agoI would be really interested to know where the overhead in the simple rust version comes from. At first I thought it might be the println() being less efficient for whatever reason, but even without that line this[1] takes 300ms, compared to 184ms for this[2] (with the printf!). 1. https://raw.githubusercontent.com/atilaneves/pythagoras/master/simple.rs https://raw.githubusercontent.com/atilaneves/pythagoras/mast... 2. https://raw.githubusercontent.com/atilaneves/pythagoras/master/simple.cpp https://raw.githubusercontent.com/atilaneves/pythagoras/mast...
- cjhanks 8y agoPerhaps the terminal is linebuffered. (edit: I forgot that `!` was a character in Rust, so I mis-read your comment, oh well).
- trishume 8y agoI looked at the disassembly of both of those in https://godbolt.org/ https://godbolt.org/ and it looks like for some reason Rust puts the loop variables x,y and z on the stack and the loads them off the stack. This causes a bunch of L1 hits instead of register hits in the tight loop, causing the 2x slowdown. I have no idea why LLVM misses optimizing this in Rust's case. Likely it is fed much trickier IR. It also misses the optimization that it does in the C case of lifting the xx and zz multiplications to the appropriate loop level.
- the_duke 8y agoSomeone found a single character change providing a 2x speedup: https://www.reddit.com/r/rust/comments/ab7hsi/comparing_pythagorean_triples_in_c_d_and_rust/ https://www.reddit.com/r/rust/comments/ab7hsi/comparing_pyth...
- jordigh 8y agoBy the way, is it a secret to programmers that Pythagorean triples can be parametrised? I understand that the point of the code is to enumerate them without knowing that they can be parametrised, but the parametrisation in this case is not so scary, so it's a fun bit of trivia for programmers (for mathematicians, it's more fundamental, as the Pythagorean parametrisation is an important prototype for counting rational points on elliptic curves, a fundamental problem in number theory). https://en.wikipedia.org/wiki/Pythagorean_triple#Generating_a_triple https://en.wikipedia.org/wiki/Pythagorean_triple#Generating_... Slightly less easy nor well-known amongst mathemticians (or at least, this is new to me), you can enumerate all primitive Pythagorean triples in a tree! https://en.wikipedia.org/wiki/Tree_of_primitive_Pythagorean_triples https://en.wikipedia.org/wiki/Tree_of_primitive_Pythagorean_... This tree is really interesting! I wonder where those matrices come from. The proof that it works is a boring mechanical check, but I'm now interested in how they were derived in the first place. This is a 20th century discovery, unlike the parametrisation which comes from antiquity. I think I'll be reading some papers about this.
- stabbles 8y agoThis was useful to me when doing Project Euler exercises, but I wouldn't know how to prove this generates all triples.
- jordigh 8y agoThe basic idea I like is, take x^2 + y^2 = z^2, but divide by z^2 to get (x/z)^2 + (y/z)^2 = 1. Now you're looking for the rational points on the unit circle. An important observation is that any two rational points define a line with rational slope. You know of one such rational point on the unit circle, say, (-1, 0). Now you can invert this process and pick a line with rational slope that goes through (-1,0). All lines with rational slope going through (-1,0) will hit another rational point on the circle, so you've found all rational points on the circle by this method, which include all primitive Pythagorean triples. When you work out this math, you recover the classical parametrisation, where the slope m/n of the line corresponds to the parameters, and this line-intersection method is a good inspiration for how to do the same with some higher-order curves such as elliptic curves, where it leads to the group law on them.
- blattimwind 8y agoIf there are big differences in where you don't expect them (e.g. printf in a for loop being very slow), then it usually is a difference in implementation and possibly contract. E.g. I wouldn't be surprised if println! in Rust is synchronous, which tends to be slow on both terminals and redirections (i.e. file I/O). For these simple examples an strace may be enlightening in this regard. Edit: Indeed this seems to be the case from the library source code (println! -> _eprint -> stdout -> LineWriter).
- ah- 8y agoYes, this benchmark would be much more interesting if it didn't do any println on the hot path and just accumulated the results and wrote them out once in the end. Edit: I've tried (println! vs a single io::stdout::lock() and writeln!), and it doesn't seem to make any difference, at least for the range.rs example. Hmm.
- steveklabnik 8y agoSometimes, the trick there is that compilers can be really good at compile time stuff, and the whole computation may just be computed at compile time. In this case? Probably not. But it's not always the right way to do a benchmark, or at least, without some tweaks.
- burntsushi 8y agoIt's because println! isn't in the hot path. So it's fine to use it here.
- steveklabnik 8y agoprintln will lock stdout for the duration of its work, yes. You can lock once before and use write! instead.
- ape4 8y agoMight be interesting if all the tests used the same library for output. After all both D and Rust can use C libraries.
- muizelaar 8y agoThe C++ version is taking a compile and runtime short cut by using printf instead of iostream. This pull request has timings with this changed: https://github.com/atilaneves/pythagoras/pull/1#issuecomment-450661579 https://github.com/atilaneves/pythagoras/pull/1#issuecomment...
- clappski 8y agoUsing the <iostream> stuff isn’t really idiomatic - on both modern and legacy code bases I’ve used operator<< overloads on custom types, but never called std::cout in a hot loop or used std::endl to do anything but actually flush the stream.
- muizelaar 8y agoWhat would you suggest is C++ recommended way for doing io?
- IshKebab 8y agoI would use std::cout but output "\n" instead of endl.
- prewett 8y agoIs std::endl guaranteed to flush the stream? I’d think you’d be better off with std::cout::flush(), std::flush if you prefer manipulators. It would certainly be clearer, assuming it’s not temporary debug code.
- wbkang 8y agoIt is guaranteed to flush.
- TheCoelacanth 8y agoIt is. That is the only reason to use it other than '\n'
- pjmlp 8y ago
- atilaneves 8y agoUpdate: The Rust versions can all be made to run faster with a simple edit. I've updated the timings and edited some of the text.
- dralley 8y agoThis was a really good and worthwhile analysis, thanks! Despite the changes people are submitting, it's good to know about these different gotchas. And there may be one or two actual compiler enhancements that could be made here also.
- vlovich123 8y agoNot mentioned in the post but I'm assuming you're using O0 for "debug"? It may be worthwhile to reconsider as O0 is now considered to be a tool for compiler authors & regular people should use -Og (or opt-level 1 for Rust) as a way of generating builds that have performance optimizations applied that don't impact debugging.
- ZirconiumX 8y agoI've usually found -Og to inline functions or optimise out variables I needed to investigate. It's happened often enough that I just use printf in release mode.
- dj-wonk 8y agoTo save a some human parsing effort in interpreting the column names: CT = compile time RT = run time
- cevn 8y agoWebsite is basically useless on mobile. Redirects to random garbage
- mastax 8y agoNot for me.
- keldaris 8y agoThose of us who have compute-bound workloads (scientific simulations in my case, but this often comes up in gamedev and elsewhere) often complain about how unusably slow debug builds impede our workflow in C++. One fairly common solution to this in extreme cases is sticking to a mostly C-like subset of the language or just using C. I don't have much experience with Rust, but judging by the results here, debug builds in Rust are effectively useless, even compared to C++. How do Rust programmers deal with this? Do you just give up and resort to printf debugging, or are there ways to improve the situation?
- kibwen 8y agoThis is going to sound unbelievable, but I don't really ever do runtime debugging of my Rust code. Nearly all my debugging happens at compile-time (which is faster to iterate with thanks to cargo-check skipping codegen and linking). That said, I do know some people with large Rust codebases who do value the ability to run debug builds, and the slowness of -O0 binaries is a sore spot for them. As with C, you can try -O1 for a compromise between runtime and compiletime.
- jordigh 8y agoThe issue is that the optimised code is not the same as the unoptimised code, and debugging optimised code sucks. It appears to the debugger that your execution is jumping around erratically and often variables don't exist because the optimiser decided that they didn't need a reason to exist. It's a problem with any kind of compiled language that runs through an optimiser, whether C, C++, Rust, or D. Maybe that's why so many people just don't use debuggers, although my philosophy is that everyone should use a debugger, but maybe they just haven't found the right one yet.
- loeg 8y agoIt doesn't have to suck nearly as badly as it does today with LLVM-based toolchains, including Rust's. With C and C++, GCC does a much better job of maintaining accurate DWARF information with performance at least as good as Clang. (And optimized code certainly shouldn't be "jumping around erratically," simply because that results in poor performance on modern CPUs with multi-cycle instruction pipelines and imperfect branch predictors.) A variable doesn't have to be in a single register throughout an entire function, or even present in registers / memory at all! DWARF is capable of representing storage locations that change over the course of a subroutine, as well as synthesizing values from other registers/memory, simple operations, and constants. Compilers can do a lot better (although certainly not 100%) than they do today without sacrificing any of the optimizations they make, and especially Clang/LLVM have a lot of room for improvement. I highly recommend this blog post on the subject: https://backtrace.io/blog/engineering/compile-once-debug-twice-picking-a-compiler-for-debuggability-1of3/ https://backtrace.io/blog/engineering/compile-once-debug-twi...
- arunc 8y agoFor the sake of history, `range` was found/coined by Andrei [1] [2] as a way to overcome the issues with the iterator pattern (in STL). I wish Eric Niebler credited Andrei somewhere, but I couldn't find any. Honest question: why should I use ranges after all? As I understand, ranges are the core of idiomatic D and they are not worth it (as per OP)? AFAIK, ranges (at least in D) are not thread-safe [3]. So what real benefits does it bring? [1] https://accu.org/content/conf2009/AndreiAlexandrescu_iterators-must-go.pdf https://accu.org/content/conf2009/AndreiAlexandrescu_iterato... [2] http://www.informit.com/articles/printerfriendly/1407357 http://www.informit.com/articles/printerfriendly/1407357 [3] https://github.com/carun/parallel-read-tester/commit/3e69da45bfe6820d3a93d28a61e1608bfbb945ed https://github.com/carun/parallel-read-tester/commit/3e69da4...
- atilaneves 8y agoI wrote the blog post and I definitely think ranges are worth using, and said as much in the link. I just don't think they're worth it in this particular case. The code is usually clearer and more reusable with ranges, and if by chance they're bottleneck (unlikely) then rewrite into raw for loops.
- arunc 8y agoThanks!
- pfultz2 8y ago> For the sake of history, `range` was found/coined by Andrei No it wasn't. Boost.Range library predates Andrei's Range talk in 2009. Boost.Range was introduced in boost 1.32, which was released in 2004: https://www.boost.org/users/history/version_1_32_0.html https://www.boost.org/users/history/version_1_32_0.html And from Boost.Range's "History and Acknowledgement" it explains where the term came from: > The term Range was adopted because of paragraph 24.1/7 from the C++ standard https://www.boost.org/doc/libs/1_32_0/libs/range/doc/history_ack.html https://www.boost.org/doc/libs/1_32_0/libs/range/doc/history... Furthermore, what is being standardized in C++ is an expansion of what is in Boost.Range, which uses iterators underneath. Andrei's term for ranges(and what is in D) are actually quite different as it removes the basis for iterators completely.
- deleted 8y ago[deleted]