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.
- 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
- Then you see a call to
rb_thread_wait_for
- Which in turn uses sleep_hrtime
- This calls
native_sleep
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/loops | Ruby | Rust | C |
1 | 1.005152 | 1.005035 | 1.005004 |
10 | 1.037259 | 1.036809 | 1.030772 |
100 | 1.195160 | 1.187932 | 1.208198 |
1000 | 1.267761 | 1.270578 | 1.265436 |
10_000 | 1.30300 | 1.296513 | 1.292802 |
100_000 | 1.49093 | 1.507843 | 1.494515 |
1_000_000 | 4.04825 | 4.125868 | 2.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.