SynchronizedDelegator for wrapping thread-unsafe objects (#8556) #405

Open
wants to merge 1 commit into
from

Conversation

2 participants

headius commented Sep 30, 2013

This implements #8556 from MRI's redmine, which calls for an additional delegator that wraps all calls in synchronization, to make them a bit more concurrency-friendly.

* lib/delegate.rb: Add SynchronizedDelegator to provide a wrapper
  for thread-unsafe data structures and objects.
* test/test_delegate.rb: Tests for SynchronizedDelegator.

avdi commented Sep 30, 2013

Given that Monitor is just sugar (and an extra object) on top of MonitorMixin, any particular reason you're not using MonitorMixin for this?

avdi commented Sep 30, 2013

...I was going to write an alternate version (and maybe a DelegateClass version as well), but for some reason I can't run tests on this branch. They keep failing with test/test_delegate.rb: cannot load such file -- thread. This is true even after a dist-clean, configure, and make. No idea what's going on...

headius commented Sep 30, 2013

I thought about using MonitorMixin too, but because this is a delegate, we don't want to define any more methods on it than we need to.

I think perhaps you need to add -I .ext/<platform> to pick up the built extensions on a local build.

avdi commented Oct 1, 2013

OK, that's a fair point.

headius commented Oct 1, 2013

Another way to reduce the object and call overhead would be to manually inline the MonitorMixin and SimpleDelegate logic into SynchronizedDelegate. That increases the possibility that a change in one might not be reflected in others, but it would improve performance by avoiding the extra object and the extra dispatches.

Here's performance numbers for the current implementation on JRuby and ruby-head. Based on this, the perf impact over a normal delegator is around 2.5x-3.5x with the current implementation.

Benchmark:

require 'benchmark'
require 'delegate'

class Foo
  def x
    self
  end
end

f = Foo.new
f2 = SimpleDelegator.new(f)
f3 = SimpleDelegator.new(f)

loop do
  puts "simple"
  puts Benchmark.measure {
    1_000_000.times do
      f2.x; f2.x; f2.x; f2.x; f2.x
    end
  }
  puts "synchronized"
  puts Benchmark.measure {
    1_000_000.times do
      f3.x; f3.x; f3.x; f3.x; f3.x
    end
  }
end

ruby head:

simple
  2.810000   0.000000   2.810000 (  2.812045)
synchronized
  8.490000   0.000000   8.490000 (  8.502126)
simple
  3.030000   0.010000   3.040000 (  3.032756)
synchronized
 10.010000   0.010000  10.020000 ( 10.024378)
simple
  2.790000   0.010000   2.800000 (  2.795635)
synchronized
 10.050000   0.010000  10.060000 ( 10.062782)
simple
  2.860000   0.000000   2.860000 (  2.861839)
synchronized
  9.790000   0.010000   9.800000 (  9.800231)
simple
  3.230000   0.000000   3.230000 (  3.229262)
synchronized
 10.180000   0.010000  10.190000 ( 10.196863)

JRuby head (with invokedynamic):

simple
  4.070000   0.110000   4.180000 (  2.231000)
synchronized
  8.320000   0.100000   8.420000 (  5.484000)
simple
  2.070000   0.020000   2.090000 (  1.833000)
synchronized
  4.010000   0.010000   4.020000 (  3.919000)
simple
  1.620000   0.000000   1.620000 (  1.539000)
synchronized
  3.640000   0.010000   3.650000 (  3.605000)
simple
  1.800000   0.010000   1.810000 (  1.780000)
synchronized
  3.980000   0.010000   3.990000 (  3.946000)
simple
  1.900000   0.000000   1.900000 (  1.755000)
synchronized
  3.980000   0.010000   3.990000 (  3.883000)
simple
  1.550000   0.000000   1.550000 (  1.519000)
synchronized
  4.220000   0.010000   4.230000 (  4.121000)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment