1. 6

I’m a Rubyist currently learning Rust and doing Project Euler problems (I’m starting number 4, so I’m pretty early in my journey, yet - code). I find myself disappointed that the Rust solutions aren’t way faster than my Ruby solutions due to the solutions being mostly algorithmic rather than having to brute force a lot of answers. This is both gratifying as a Rubyist and frustrating as someone who just really wanted to see Rust kick ass 😛

What kinds of problems would Rust shine at that Ruby (or Python, etc.) would stutter on? Preferably toy problems, but it’s understandable if “toy problem” and “difficult for high level programming languages” is a small set. Thanks!

  1.  

  2. 9

    Ray Tracing in One Weekend should fit your constraints nicely.

    1. 2

      It’s a good book. There are some typos and omissions in the original C++ code, but it translated well to Rust. Sped up a lot with judicious use of rayon, too.

    2. 5

      I would suggest processing and extracting information from large files like log files, CSV, or JSON. See this post for an example.

      1. 1

        Do you happen to know where I could come across any spare logs or similar files?

        1. 2

          Just generate some with random garbage data, and then do something numerically intensive like summing it, counting words, or similar.

          1. 2

            Maybe you could download some books from https://archive.org/index.php

            1. 2

              Install bro and crawl the internet. Seriously.

          2. 4

            A lot of the Project Euler problems are math heavy and use very simple code structures. In such situations the resulting instructions of a compiled/interpreted language tend to be similar enough to not make a big difference in execution time.

            I’d think that in many cases the interpreter startup time makes the biggest difference.

            If you want to see a bigger difference I think that might happen in more complex problems or outside of Project Euler because one of Rust’s frequently mentioned selling points is zero cost abstractions. That’s where Ruby sometimes feels slower than it is. Take Rails for example. There if you profile the code the cost of abstraction becomes visible.

            Not knowing enough about Ruby and Rusts implementation I’d suggest trying to parse big files, maybe even reorganizing one to show a bigger difference. Some structured data. Maybe something like converting text to image or vice versa.

            However the bigger difference will be seen in bigger projects with more pieces working together.

            Other than that you might consider adding something like memory usage into the equation. That might make a big difference on small projects.

            1. 2

              Another option is to continue with the PE problems in Hackerrank. They often add more data and are way more strict about runtime than PE (where it’s more of a honor system anyway). But I don’t know if Hackerrank has a Rust environment yet…

            2. 3

              Are you compiling your code in release mode? There’s a significant performance difference. I found that rust’s speed made certain brute force approaches to PE problems more viable.

              1. 1

                That is an excellent point, I’ll make sure to try that, although the perceptual differences are fairly tiny even in debug 😬

              2. 3

                I’m surprised your code for #2 isn’t faster in Rust — I’ll try it on my machine and see if I can figure out why. For #1 the data set is pretty small, so I’m not surprised there isn’t an appreciable difference. For #3 you’re printing log lines to stdout in each loop iteration, so you’re probably IO-bound in both and are benchmarking your OS/PTY/terminal emulator more than the different languages :)

                1. 5

                  Okay, I tested out your solution to #2: on my machine, the Rust version when compiled in release mode finishes in 0.01s (with rustc 1.28.0, which is a little out of date but not horrifically so), and the Ruby version finishes in 0.24s (with Ruby 2.5.3p105 from the Brightbox PPA, not sure what version you’re using). The debug version is also very fast for Rust: time ./target/debug/problem-02 finishes in 0.01s as well. But, running the Rust version with cargo run is noticeably slower: time cargo run finishes in 0.11s, which is only ~2x faster than the Ruby version (rather than an order of magnitude faster).

                  I’m guessing there’s one of a few possible things happening for you:

                  1. Our machines are very different! This is totally possible. My initial testing was done on an Ubuntu 16.04 install running on Windows Subsystem for Linux, which is a bonkers way to do Unixy benchmarking: many syscalls are much slower on WSL, for example. I sanity checked the numbers on a bare-metal Ubuntu 17.10 server I have lying around, though, and the Rust version was still an order of magnitude faster than the Ruby version in both debug and release modes.
                  2. You’re mostly testing with cargo run, and noticing that it’s slow. It seems like Cargo is doing some relatively slow initialization (probably checking if the compiled binary is up-to-date with the source code on disk), which throws off your benchmarking. With a big enough dataset size you’ll still eventually see the order of magnitude runtime difference show up, but when it’s measured in a couple hundred ms, boot time checks can make a big difference. FWIW, cargo run also by default uses debug mode, which will sometimes be slower (although in this case it seems like even debug mode is quite a bit faster than the Ruby version).
                  3. Or, you’re mostly eyeballing the performance differences, and noticing that Ruby feels fast enough and it doesn’t matter that the Rust version is 10x faster: it’s all finishing in way less than a second anyway. That’s a fair point: most programs don’t do enough work that language choice will be super noticeable to humans. Crunching huge logfiles or doing raytracing on the CPU like other posters have mentioned will definitely feel noticeably different for Rust programs than Ruby ones, if you’re looking for perceptually-different experiences.

                  Update: For better data I switched over to using hyperfine to benchmark. It claimed (with a --warmup 5 set to avoid penalizing Ruby for warmup file caches) the following for the different problems:

                  Chart for visualizing the data.

                  #1
                  rust (direct call, compiled with --release): 15.9ms
                  rust (direct call, compiled with default debug mode): 21.0ms
                  rust (cargo run): 98.3ms
                  ruby 2.5: 190.1ms
                  
                  #2
                  rust (direct call, compiled with --release): 23.8ms
                  rust (direct call, compiled with default debug mode): 15.9ms
                  rust (cargo run): 98.5ms
                  ruby 2.5: 188.3ms
                  
                  #3
                  rust (direct call, compiled with --release): 44.6ms
                  rust (direct call, compiled with default debug mode): 145.7ms
                  rust (cargo run): 226.4ms
                  ruby 2.5: 379.5ms
                  

                  Interesting that the debug version for #2 was actually faster than the release version; I guess for very small programs sometimes the optimizer makes the wrong choices. There’s a pretty huge difference for #3 though, which seems like a heavier workload for both languages.

                  My guess is that Ruby is actually bound by VM initialization time for problems 1 and 2, since its runtimes for both are suspiciously similar despite the Rust versions being measurably different. For problem 3 there’s clearly a significant difference between Rust and Ruby that we can’t chalk up to boot time (since we can infer from the other two that boot time is probably at most ~190ms), which disproves my hypothesis that you were IO-bound :)

                  1. 1

                    All good points! I would guess it mostly comes down to #2 and #3. None of my benchmarking is rigorous (thus far). I was just hoping Rust would magically make brute force solutions to Project Euler workable. But alas…

                    1. 2

                      Well, the good news is it looks like it’ll make them around 10x more workable :) But if you’re off by a factor of a thousand, you’ll still need to break out your computer science bag of tricks for efficient algorithmic solutions.

                  2. 1

                    Ah, yes, excellent point regarding stdout 🤦‍♂️

                  3. 3

                    I once was asked to “somewhat optimize” a finely tuned program written in Java because it was starting to take a bit more than its maximum allowed “8 hours in the night”.

                    Once I figured out what it was actually doing I rewrote it in Ruby in a few days and it completed in less than 40 minutes. Basically I did everything in memory and only wrote output files at the end. That “slowness” was probably not a Java problem, but a Java-programmer problem. Maybe rewriting in C or Rust would squeeze out even more performance, but going from 8 hours to 40 minutes was more than enough.

                    Some time later I was tasked to “somewhat optimize” a program written in C that loaded some 45+ millions records in memory (topping at almost 5 Gb RAM usage), did its wonders, then saved the output (I guess someone there just appled my method). In a 16 Gb server they could only launch three copies, and a RAM upgrade was out of question because it would have taken a few weeks. They expected me to shrink the thing to fit in less than 4 Gb, to be able to start a fourth copy in those 16 Gb. Yes, they never heard about shared memory. I just added shm and a few semaphores. A few weeks later they told me they were happy even if they didn’t get significant improvements with more than 24 parallel copies of that program running.

                    Strictly speaking, on small things Rust isn’t “faster than C”: that only relates to what LLVM optimizer can do on the intermediate code produced by gcc and rustc. On medium-sized things, Rust “may be faster than C” because rustc intermediate code may rely more on registers than memory accesses. On larger things, it all depends on the programmer knowing simple things like: “sorting that array has its worst case when the array is almost already ordered”, or things like: “shared memory actually exists and it’s even supported by the operating systems and standard libraries”.

                    1. 2

                      Rust “may be faster than C” because rustc intermediate code may rely more on registers than memory accesses

                      Could you elaborate on that a little? Why would rustc generate IR that requires less memory accesses than equivalent C?

                      1. 2

                        I once was asked to “somewhat optimize” a finely tuned program written in Java

                        No-one ever claimed that the Java program I was tasked with baby-sitting was “finely tuned” - it took around 3 days to import a CD-rom’s worth of XML into a database (this was in the late 90s).

                        The problem was that there was no error checking so that it would randomly die due to incorrectly escaped ampersands and the like.

                        I started doing some sanity checking in Perl and somehow wound up with something that actually managed to import all the data in (IIRC) 2 hours, then lost all the source when I managed to “rm -rf” the wrong directory over a slow SSH link. But by then I was on another project and I never really did find out what happened to the import…