-
-
Save tenderlove/098cb892f54b131e4e7520ce49057c3f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## | |
# 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 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
@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)?