rubymultithreadingmri

MRI ruby threading and performance


My first question on SO, but I've lurked for a long time now so you'll have to forgive me if I've broken any rules or posted a rubbish question.

I'm trying to get a better understanding of threading and I decided to test MRI and see how it performs in general.

Given the following code (and output), why are the threaded operations so much slower than the non-threaded variant?

code

class Benchmarker
  def self.go
    puts '----------Benchmark Start----------'
    start_t = Time.now
    yield
    end_t = Time.now
    puts "Operation Took: #{end_t - start_t} seconds"
    puts '----------Benchmark End------------'
  end
end

# using mutex
puts 'Benchmark 1 (threaded, mutex):'
Benchmarker.go do
  array = []
  mutex = Mutex.new
  5000.times.map do
    Thread.new do
      mutex.synchronize do
        1000.times do
          array << nil
        end
      end
    end
  end.each(&:join)
  puts array.size
end

# using threads
puts 'Benchmark 2 (threaded, no mutex):'
Benchmarker.go do
  array = []
  5000.times.map do
    Thread.new do
      1000.times do
        array << nil
      end
    end
  end.each(&:join)
  puts array.size
end

# no threads
puts 'Benchmark 3 (no threads):'
Benchmarker.go do
  array = []
  5000.times.map do
    1000.times do
      array << nil
    end
  end
  puts array.size
end

the output

Benchmark 1 (threaded, mutex):
----------Benchmark Start----------
5000000
Operation Took: 3.373886 seconds
----------Benchmark End------------
Benchmark 2 (threaded, no mutex):
----------Benchmark Start----------
5000000
Operation Took: 5.040501 seconds
----------Benchmark End------------
Benchmark 3 (no threads):
----------Benchmark Start----------
5000000
Operation Took: 0.454665 seconds
----------Benchmark End------------

Thanks in advance.


Solution

  • Once you hit a high amount of threads (5000), the amount of overhead for switching between threads by the scheduler far outweighs the amount of work that each thread actually does. Typically you want 30-50 threads max.

    Try lowering the amount of threads and proportionally increasing the amount of work that each does:

      20.times.map do
        Thread.new do
          250000.times do
            array << nil
          end
        end
      end.each(&:join)
    

    and you should see far more comparable results.

    Note you will probably see the lower bound Time(threaded) >= Time(non-threaded) - that is the Threaded version's time can't be lower than single-threaded version. This is because of the MRI's GIL which allows only one thread to execute at a time (they can never run in parallel). Some ruby implementations such as JRuby allow parallel execution of threads.