Skip to content

Instantly share code, notes, and snippets.

@tenderlove
Last active October 31, 2024 17:56
Show Gist options
  • Save tenderlove/098cb892f54b131e4e7520ce49057c3f to your computer and use it in GitHub Desktop.
Save tenderlove/098cb892f54b131e4e7520ce49057c3f to your computer and use it in GitHub Desktop.
##
# Test performance of CPU bound work mixed with IO bound work using different
# concurrency primitives in Ruby.
#
# Output on my machine:
#
# $ ruby sched.rb
# No parallelism: 2.73 seconds
# Async (CPU first): 2.76 seconds
# Async (IO first): 1.39 seconds
# Thread (CPU first): 1.49 seconds
# Thread (IO first): 1.38 seconds
#
# Fiber completion time is very different depending on what workload is
# scheduled first. Fiber's aren't preemptable, meaning any CPU bound work
# will prevent the Fiber from switching - even if there is another Fiber that
# _could_ execute in parallel with the current Fiber.
#
# Threads are preemptable, so even if there is a CPU bound task currently
# running, the thread scheduler can find IO bound threads and execute them
# in parallel. Note that threads are faster in both cases, whether CPU
# bound work is scheduled first or not.
require "async"
def measure
x = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield
Process.clock_gettime(Process::CLOCK_MONOTONIC) - x
end
def fib(n)
if n < 2
n
else
fib(n-2) + fib(n-1)
end
end
# Find a fib that takes ~1s
fib_i = 50.times.find { |i| measure { fib(i) } >= 1 }
io_time = measure { fib(fib_i) }
time = measure {
fib(fib_i)
sleep(io_time)
}
puts "No parallelism: #{sprintf("%.2f", time)} seconds"
Async {
time = measure {
x = Async { fib(fib_i) }
y = Async { sleep(io_time) }
x.wait
y.wait
}
puts "Async (CPU first): #{sprintf("%.2f", time)} seconds"
}
Async {
time = measure {
y = Async { sleep(io_time) }
x = Async { fib(fib_i) }
x.wait
y.wait
}
puts "Async (IO first): #{sprintf("%.2f", time)} seconds"
}
time = measure {
x = Thread.new { fib(fib_i) }
y = Thread.new { sleep(io_time) }
x.join
y.join
}
puts "Thread (CPU first): #{sprintf("%.2f", time)} seconds"
time = measure {
y = Thread.new { sleep(io_time) }
x = Thread.new { fib(fib_i) }
x.join
y.join
}
puts "Thread (IO first): #{sprintf("%.2f", time)} seconds"
@Fryguy
Copy link

Fryguy commented Oct 30, 2024

@tenderlove Does it matter that you did x.wait then y.wait in both order of cases (i.e. you switched the definition order but not the wait order)?

@tenderlove
Copy link
Author

@Fryguy no. The only thing that matters is the order in which work gets scheduled. Practically speaking, we have to assume that work is actually scheduled in a random order. It just so happens that on CRuby it'll get scheduled in the order we declared in this code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment