Skip to content

Adds a JRuby optimized version of Concurrent::Atom #441

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 21, 2015

Conversation

lucasallan
Copy link
Member

Non-optimized version

~~~ Ruby version: 2.2.2
~~~ JRuby version: 9.0.1.0
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
Before JVM warm-up...
   4.720000   0.030000   4.750000 (  4.103692)
After JVM warm-up...
   3.920000   0.020000   3.940000 (  3.925786)

Optimized version

~~~ Ruby version: 2.2.2
~~~ JRuby version: 9.0.1.0
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
Before JVM warm-up...
   2.980000   0.020000   3.000000 (  2.742403)
After JVM warm-up...
   2.440000   0.010000   2.450000 (  2.479192)

Benchmark script:

#!/usr/bin/env ruby

require 'concurrent'
require 'benchmark'

NUM      = 50_000_000
WARM_UPS = NUM

def jruby?
  defined? JRUBY_VERSION
end

def version
  gem_dir = Gem::Specification.find_by_name('concurrent-ruby').gem_dir
  File.open(File.join(gem_dir, 'VERSION'), 'r').read
rescue
  'unknown'
end

puts "~~~ Ruby version: #{RUBY_VERSION}"
if jruby?
  puts "~~~ JRuby version: #{JRUBY_VERSION}"
else
  puts "~~~ Ruby engine: #{RUBY_ENGINE}"
end
puts "~~~ Gem version: #{version}"

Benchmark.bm do |stats|

  puts "Benchmarking #{Concurrent::Atom}..."

  ref   = Concurrent::Atom.new('foo')
  value = nil

  puts 'Before JVM warm-up...' if jruby?
  stats.report do
    NUM.times { value = ref.value }
  end

  if jruby?
    WARM_UPS.times { value = ref.value }

    puts 'After JVM warm-up...'
    stats.report do
      NUM.times { value = ref.value }
    end
  end
end

require 'concurrent/concern/observable'
require 'java'

java_import 'java.util.concurrent.atomic.AtomicReference'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, how big is the perf hit of using Concurrent::Reference versus the Java one?

@jdantonio
Copy link
Member

@lucasallan Thank you very much for working on this. I'm having difficulty following the differences between the implementations. AFAICT the main difference is that the newer version simply uses java.util.concurrent.atomic.AtomicReference directly rather than Concurrent::AtomicReference. Is that correct?

@lucasallan
Copy link
Member Author

@jdantonio This version is directly based on Clojure code[1], but I guess we should be using Concurrent::AtomicReference rather than java.util.concurrent.atomic.AtomicReference.
But before it wasn't using AtomicReference anyway, right?

[1] - https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Atom.java

@jdantonio
Copy link
Member

I didn't look at the Clojure code before implementing this or Agent. I worked entirely from the documented behavior. I probably should have looked at the Clojure source. :-) If the implementation can be improved following that example I'm all for it, but can we improve it for the base implementation? Do we need a JRuby-specific version?

@lucasallan
Copy link
Member Author

If we build an implementation around AtomicReference then we don't need a JRuby optimized version, since AtomicReference is already jruby-optimized.

I'll work on it during this week and let you know the results.

@jdantonio
Copy link
Member

@lucasallan Thank you!

@lucasallan
Copy link
Member Author

@jdantonio Results:

JRuby Using Concurrent::Atomic

➜  concurrent-ruby git:(master) ✗ ruby ../atom_bench.rb
~~~ Ruby version: 2.2.2
~~~ JRuby version: 9.0.1.0
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
Before JVM warm-up...
   4.190000   0.040000   4.230000 (  3.116446)
After JVM warm-up...
   2.840000   0.010000   2.850000 (  2.848073)


➜  concurrent-ruby git:(master) ✗ ruby ../atom_bench.rb
~~~ Ruby version: 2.2.2
~~~ JRuby version: 9.0.1.0
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
Before JVM warm-up...
   2.860000   0.020000   2.880000 (  2.546445)
After JVM warm-up...
   2.400000   0.010000   2.410000 (  2.395978)



➜  concurrent-ruby git:(master) ✗ ruby ../atom_bench.rb
~~~ Ruby version: 2.2.2
~~~ JRuby version: 9.0.1.0
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
Before JVM warm-up...
   2.620000   0.020000   2.640000 (  2.438905)
After JVM warm-up...
   2.500000   0.010000   2.510000 (  2.504156)



➜  concurrent-ruby git:(master) ✗ ruby ../atom_bench.rb
~~~ Ruby version: 2.2.2
~~~ JRuby version: 9.0.1.0
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
Before JVM warm-up...
   2.740000   0.010000   2.750000 (  2.386496)
After JVM warm-up...
   2.390000   0.010000   2.400000 (  2.376449)

JRuby Using concurrent-ruby-0.9.1(Current implementation):

➜  concurrent-ruby git:(master) ✗ ruby ../atom_bench.rb
~~~ Ruby version: 2.2.2
~~~ JRuby version: 9.0.1.0
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
Before JVM warm-up...
   4.690000   0.040000   4.730000 (  4.067498)
After JVM warm-up...
   3.690000   0.010000   3.700000 (  3.705187)


➜  concurrent-ruby git:(master) ✗ ruby ../atom_bench.rb
~~~ Ruby version: 2.2.2
~~~ JRuby version: 9.0.1.0
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
Before JVM warm-up...
   5.000000   0.030000   5.030000 (  4.547322)
After JVM warm-up...
   4.200000   0.010000   4.210000 (  4.217589)


➜  concurrent-ruby git:(master) ✗ ruby ../atom_bench.rb
~~~ Ruby version: 2.2.2
~~~ JRuby version: 9.0.1.0
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
Before JVM warm-up...
   4.710000   0.020000   4.730000 (  4.314722)
After JVM warm-up...
   4.290000   0.010000   4.300000 (  4.258959)


➜  concurrent-ruby git:(master) ✗ ruby ../atom_bench.rb
~~~ Ruby version: 2.2.2
~~~ JRuby version: 9.0.1.0
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
Before JVM warm-up...
   4.320000   0.020000   4.340000 (  3.944181)
After JVM warm-up...
   3.820000   0.020000   3.840000 (  3.842717)

MRI

MRI Using Concurrent::Atomic

➜  concurrent-ruby git:(master) ruby ../atom_bench.rb
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
  18.470000   0.040000  18.510000 ( 18.550063)

➜  concurrent-ruby git:(master) ruby ../atom_bench.rb
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
  18.510000   0.030000  18.540000 ( 18.586338)

➜  concurrent-ruby git:(master) ruby ../atom_bench.rb
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
  18.460000   0.050000  18.510000 ( 18.567479)

➜  concurrent-ruby git:(master) ruby ../atom_bench.rb
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
  18.500000   0.050000  18.550000 ( 18.611711)

MRI Using concurrent-ruby-0.9.1(Current implementation):

➜  concurrent-ruby git:(master) ruby ../atom_bench.rb
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
  16.340000   0.050000  16.390000 ( 16.473306)

➜  concurrent-ruby git:(master) ruby ../atom_bench.rb
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
  16.430000   0.070000  16.500000 ( 16.622789)

➜  concurrent-ruby git:(master) ruby ../atom_bench.rb
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
  16.440000   0.070000  16.510000 ( 16.620927)

➜  concurrent-ruby git:(master) ruby ../atom_bench.rb
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
  16.880000   0.100000  16.980000 ( 17.167879)

MRI with C extensions

MRI EXT Using Concurrent::Atomic

➜  concurrent-ruby git:(master) ruby ../atom_bench.rb                                     
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
   6.090000   0.010000   6.100000 (  6.120272)

➜  concurrent-ruby git:(master) ruby ../atom_bench.rb
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
   6.130000   0.020000   6.150000 (  6.176619)

➜  concurrent-ruby git:(master) ruby ../atom_bench.rb
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
   6.110000   0.020000   6.130000 (  6.143121)

➜  concurrent-ruby git:(master) ruby ../atom_bench.rb
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
   6.150000   0.020000   6.170000 (  6.199732)

MRI EXT Using concurrent-ruby-0.9.1(Current implementation):

➜  open-source  ruby atom_bench.rb 
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
   6.490000   0.020000   6.510000 (  6.530063)

➜  open-source  ruby atom_bench.rb
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
   6.370000   0.060000   6.430000 (  6.579451)

➜  open-source  ruby atom_bench.rb
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
   6.240000   0.020000   6.260000 (  6.292967)

➜  open-source  ruby atom_bench.rb
~~~ Ruby version: 2.2.3
~~~ Ruby engine: ruby
~~~ Gem version: unknown
       user     system      total        real
Benchmarking Concurrent::Atom...
   6.290000   0.020000   6.310000 (  6.340481)

@jdantonio
Copy link
Member

Thank you!

jdantonio added a commit that referenced this pull request Oct 21, 2015
Adds a JRuby optimized version of Concurrent::Atom
@jdantonio jdantonio merged commit cd158c2 into ruby-concurrency:master Oct 21, 2015
@pitr-ch
Copy link
Member

pitr-ch commented Oct 22, 2015

@lucasallan I very appreciate your contributions but this particular change is unsafe. The @State instance variable is not properly initialized. Generally speaking I think all of our abstractions should use synchronisation layer to avoid this type of problems. It also helps greatly to solve issues across the gem if one is found. I know the benchmark shows improvement but when the number of iterations is taken into-account it's not big gain. It's also best to avoid optimisations of a method inlining kind, that's a job for the Ruby interpreter. We should prefer code clarity and algorithmic optimisations. I would suggest to revert this change.

@lucasallan
Copy link
Member Author

@pitr-ch Could you clarify what you meant by is not properly initialized? Also, we're using Concurrent:: AtomicReference which already give us some benefits + performance, I don't understand why it would be a bad idea.

@pitr-ch
Copy link
Member

pitr-ch commented Oct 23, 2015

Compiler or processor can reorder writes, in particular it may choose to reorder write to @state variable with write of the newly created Atom object to a variable accessible by other threads. If it does than another thread may observe Atom object with @State variable still uninitialized which would lead to #value method failing on nil error.

There is nothing bad about Concurrent::AtomicReference, in fact attr_volatile_with_cas uses it to implement CAS operations over the state field. The problematic part is that this is already abstracted pattern and it's expected to be different in future based on Ruby implementation. Some Ruby implementations may implement this directly (CAS operations over instance variables) which will allow to get rid of the indirection caused by AtomicReference. When this happens this implementation would not get the benefit of the abstraction provided by the layer, but it would have to be modified again to use the layer.

@lucasallan
Copy link
Member Author

Fair enough. Thanks for the awesome explanation @pitr-ch - I'll revert it later today.

@pitr-ch
Copy link
Member

pitr-ch commented Oct 23, 2015

Thanks @lucasallan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants