
Comparing Pythagorean triples in C++, D, and Rust - atilaneves
https://atilanevesoncode.wordpress.com/2018/12/31/comparing-pythagorean-triples-in-c-d-and-rust/
======
jordigh
By 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_...](https://en.wikipedia.org/wiki/Pythagorean_triple#Generating_a_triple)

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_...](https://en.wikipedia.org/wiki/Tree_of_primitive_Pythagorean_triples)

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
This was useful to me when doing Project Euler exercises, but I wouldn't know
how to prove this generates _all_ triples.

~~~
jordigh
The 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.

------
keldaris
Those 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
This 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.

~~~
marksomnian
How do you debug logic errors in that case? I imagine that most logic errors
can't be caught by the compiler, although I could be wrong.

~~~
staticassertion
Most logic errors can definitely be caught by the compiler if you're taking
the approach of encoding your logic into your type system. I've done this with
real codebases (in Python as well, actually, using Mypy) and it's been very
effective.

~~~
marksomnian
Can it catch something like accidentally writing "x == y", when you meant to
write "x != y"? I feel like this kind of bug can be insidious enough to waste
lots of debugging time, yet difficult to catch until you run the code.

~~~
staticassertion
Not easily. Rust's type system has limits, and what you're talking about is
something you'd probably want dependent types for.

But it can catch lots of other logic errors.

The approach I have used in both mypy and rust is to encode state transitions
into types. It's more elegant in Rust, and safer as well, but even with mypy
it means I could be very sure about certain properties of my service (it was a
sensitive service and I needed to be reasonably sure it wouldn't end up in an
invalid state).

[https://insanitybit.github.io/2016/05/30/beyond-memory-
safet...](https://insanitybit.github.io/2016/05/30/beyond-memory-safety-with-
types)

This is an example of the pattern.

When you take a "Type driven" approach to your code - stating your constraints
upfront, and encoding them into your types - you can push a lot of the debug
cycle to your compiler.

------
atilaneves
Update: 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.

~~~
vlovich123
Not 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
I'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.

------
muizelaar
The 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...](https://github.com/atilaneves/pythagoras/pull/1#issuecomment-450661579)

~~~
clappski
Using 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.

~~~
prewett
Is 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.

~~~
TheCoelacanth
It is. That is the only reason to use it other than '\n'

------
dj-wonk
To save a some human parsing effort in interpreting the column names:

    
    
      CT = compile time
      RT = run time

------
orf
I 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/mast...](https://raw.githubusercontent.com/atilaneves/pythagoras/master/simple.rs)

2\.
[https://raw.githubusercontent.com/atilaneves/pythagoras/mast...](https://raw.githubusercontent.com/atilaneves/pythagoras/master/simple.cpp)

~~~
the_duke
Someone found a single character change providing a 2x speedup:
[https://www.reddit.com/r/rust/comments/ab7hsi/comparing_pyth...](https://www.reddit.com/r/rust/comments/ab7hsi/comparing_pythagorean_triples_in_c_d_and_rust/)

~~~
bluejekyll
To quote that reddit post, which I think has some good interesting details
about why:

“I believe the problem with ..= is that x..(y + 1) is not identical with
x..=y. They are nearly equivalent on most values for x and y, except when y ==
i32::max(). At that point, x..=y should still be able to function, while x..(y
+ 1) is allowed to do nothing.”

~~~
SlowRobotAhead
I don't know anything about Rust, but is it part of it's "safety" that it has
operators to allow rollover of type max size and others that do not?

As a C programmer, that sort of feels like cheating ;)

~~~
steveklabnik
Safety means "memory safety". Integer overflow cannot cause memory unsafety
directly, and so it's safe.

(safe) Rust does not claim to make your code correct, it claims that it will
not have undefined behavior.

We _do_ care about helping you make your code correct; it will check for
overflow and panic at runtime in debug builds. But it's a secondary concern.
Memory safety is hard enough!

------
blattimwind
If 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-
Yes, 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
Sometimes, 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.

------
steveklabnik
The reddit thread already has one good relevant comment
[https://www.reddit.com/r/rust/comments/ab7hsi/comparing_pyth...](https://www.reddit.com/r/rust/comments/ab7hsi/comparing_pythagorean_triples_in_c_d_and_rust/)

~~~
stabbles
Well, 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
Possibly! 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
Looking 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-35...](https://doc.rust-
lang.org/src/core/iter/range.rs.html#340-353)

[2]
[https://github.com/JuliaLang/julia/blob/master/base/range.jl...](https://github.com/JuliaLang/julia/blob/master/base/range.jl#L591-L598)

------
arunc
For 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_iterato...](https://accu.org/content/conf2009/AndreiAlexandrescu_iterators-
must-go.pdf)

[2]
[http://www.informit.com/articles/printerfriendly/1407357](http://www.informit.com/articles/printerfriendly/1407357)

[3] [https://github.com/carun/parallel-read-
tester/commit/3e69da4...](https://github.com/carun/parallel-read-
tester/commit/3e69da45bfe6820d3a93d28a61e1608bfbb945ed)

~~~
atilaneves
I 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
Thanks!

------
cevn
Website is basically useless on mobile. Redirects to random garbage

~~~
mastax
Not for me.

