Kotlin/JVM, Rust, and Randy Random

In this previous article I implemented a small, enterprisy benchmark to compare the concept of garbage collection used by Kotlin/JVM with the concept of Rust, which claims to not have any garbage collector at all. My conclusion was, that for a moderate increase in the complexity of the programming language the benchmark in Rust performed roughly 3 times faster than using Kotlin on the JVM. A colleague of mine, who uses Rust for some time now, looked at the code and gave me some hints for further speed improvements.

My benchmark consists of a computation on a large set of randomly generated entities. In contrast to Kotlin, the memory management in Rust is pretty thin. This means, that most of the computation time is used up by the random number generator (RNG).

The standard RNG in Rust can be used as cryptographically secure pseudo-random number generator. It uses the HC-128 algorithm with a seed gof 256-bit. Looking at the source code, Kotlin on the JVM uses the standard RNG of the Java SDK. It uses a 48-bit seed and is not cryptographically secure. Comparing the performance of Kotlin with its simple and fast RNG and Rust with its sophisticated RNG seems not fair. Therefor we changed the RNG in Rust and redid our measurements.

Small is faster

This is the api to access the standard RNG in Rust:

let index = rand::thread_rng().gen_range(0, char_pool.len())

In the rand module, there is the SmallRng which is not cryptographically secure but much faster. We can create an instance locally and initialize it using the standard RNG:

let mut thread_rng = thread_rng();
let mut rng = SmallRng::from_rng(&mut thread_rng).unwrap();
let index = rng.gen_range(0, char_pool.len());

The efficiency is increased even more by creating only one instance of SmallRng, store it in an object, and pass the object as parameter to the computations. This code fragments depicts the idea:

struct RandyRandom {
    rng: Box<dyn RngCore>,
    pool: Vec<char>,
    dist: Uniform<usize>,
}

impl RandyRandom {
    fn new(rng: Box<dyn RngCore>) -> RandyRandom {
        ...
        let dist = Uniform::from(0..pool.len());
        RandyRandom { rng, pool, dist }
    }
    ...
    fn create_random_string_of_80_chars(&mut self) -> String {
       let index = self.rng.sample(self.dist);
       ...
    }
}
...
fn main() {
  let mut r = RandyRandom::new(Box::new(SmallRng::from_entropy()));
  ...
}

Let’s see if it is worthwhile

I redid the measurements from my last article and measure the run time for different amounts of entities. Here are the results using Kotlin/JVM and Rust with the three different implementation:

Switching from the standard RNG to the small RNG gives a performance boost of a factor of roughly 2.5. Reusing the RNG by passing an object gives a speedup of 10% for more then 10^5 entities.

This means the Rust implementation is nearly 10 times faster than the Kotlin/JVM variant in this benchmark. IMHO this is pretty impressive.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.