Sleep granularity delay with Ruby, Rust and C

In my hobby petrol station simulation project that uses threads and custom timer, I’m having an issue with stability of results. When my simulation speed is too high, the code seems to break. I don’t know why yet but the first thing I checked was how Ruby keeps in time with its own sleep method.

What happens when you call sleep? It’s a long story.

  1. Definition of Kernel#sleep is here https://github.com/ruby/ruby/blob/v3_3_0/process.c#L5056, which we see is implemented in c in the function rb_f_sleep
  2. Then you see a call to rb_thread_wait_for
  3. Which in turn uses sleep_hrtime
  4. This calls native_sleep
  5. native_sleep is platform dependent and implemented in thread_pthread.c and thread_win32.c for Posix and Windows systems respectively.

Here was the inspiration to check the above but it was outdated.

Anyway, I didn’t solve my issue yet but I checked something interesting. Look, the code is self explanatory:

Results for Ruby

def mysleep(n)
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  n.times { sleep(1.0/n) }
  finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)

  puts(finish - start)
end

mysleep(1)
mysleep(10)
mysleep(100)
mysleep(1000)
mysleep(10_000)
mysleep(100_000)
mysleep(1_000_000)

# output
1.0052269999869168
1.045299999997951
1.192345000046771
1.2702450000215322
1.3044280000030994
1.4968319999752566
4.050235999980941

In short, time takes time! Sleeping has it costs etc, because they all should ideally take 1 sec.

The problems start around 1-microsecond granularity. But my project has issues with 1-millisecond. Although the rise is by 25% the change overall in simulation is different. I still need to check what is wrong. I hope to find out!

At this point you might think: “OK, what if we did it with a real, compiled language? Let’s say Rust”. Why? Rust. Maybe as a tribute to the new RubyVM::YJIT compiler? Yeah, rustc is closer to Rubists than before!

Results for Rust

use std::time::{Duration, Instant};

fn mysleep(n: u32) {
    let start = Instant::now();
    for _ in 0..n {
        std::thread::sleep(Duration::from_secs_f64(1.0 / n as f64));
    }
    let finish = start.elapsed();
    println!("{:.6}", finish.as_secs_f64());
}

fn main() {
    mysleep(1);
    mysleep(10);
    mysleep(100);
    mysleep(1_000);
    mysleep(10_000);
    mysleep(100_000);
    mysleep(1_000_000);
}

But no! The result are worse than in Ruby! (Or am I doing something wrong?)

Time elapsed: 1.275852625s # for 1_000 * 1.millisecond sleep
Time elapsed: 4.142652791s # for 1_000_000 * 1.microsecond sleep

# compared to Ruby's
b = Time.now; 1000.times { sleep(0.001) }; a = Time.now; puts a - b # 1.26748
b = Time.now; 1_000_000.times { sleep(0.000_001) }; a = Time.now; puts a - b # 4.091429

But this is not to say Ruby is faster than Rust. I don’t really know what’s happening under the hood.

Results for C

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>

void mysleep(unsigned int n) {
    struct timeval start, end;
    gettimeofday(&start, NULL);

    for(unsigned int i = 0; i < n; ++i) {
        usleep((useconds_t)(1e6 / n)); // Sleep for 1/n seconds, converted to microseconds
    }

    gettimeofday(&end, NULL);
    long seconds = (end.tv_sec - start.tv_sec);
    long useconds = (end.tv_usec - start.tv_usec);
    double elapsed = seconds + useconds*1e-6;

    printf("%.6f\n", elapsed);
}

int main() {
    mysleep(1);
    mysleep(10);
    mysleep(100);
    mysleep(1000);
    mysleep(10000);
    mysleep(100000);
    mysleep(1000000);
    return 0;
}

And the results calmed me down:

Time elapsed: 1.261072 seconds # for 1_000 times 1.millisecond
Time elapsed: 2.843234 seconds # for 1_000_000 times 1.microsecond

Final countdown

ruby 3.3.0
rustc 1.76.0
Apple clang version 14.0.0

cycles/loopsRubyRustC
11.0051521.0050351.005004
101.0372591.0368091.030772
1001.1951601.1879321.208198
10001.2677611.2705781.265436
10_0001.303001.2965131.292802
100_0001.490931.5078431.494515
1_000_0004.048254.1258682.848947

Summary

Is there something wrong with my code that Rust is slower than Ruby? Should I use some flag or something, or is there a better way to write it? ChatGPT helped with both Rust and C code, so maybe there are better ways.

Of course, Ruby uses C under the hood for sleeping as shown at the beginning. And by the way, doing ruby --yjit didn’t make Ruby code faster. So maybe that’s the simple answer, that the overhead of interpreter plus C native sleep execution is still faster that the one written in Rust. I don’t know. Low level warning.

Leave a Reply

Your email address will not be published. Required fields are marked *