-
Notifications
You must be signed in to change notification settings - Fork 417
AtomicMarkableReference: Add pure Ruby implementation #281
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
AtomicMarkableReference: Add pure Ruby implementation #281
Conversation
@ianks first, let me welcome you to the group of concurrent-ruby contributors. Couple of questions/comments/concerns:
|
How about this? Also, now that we've merged #274 can we put this under the |
All I really meant was that by returning a reference to
Maybe I am understanding this wrong, but Keep in mind the main thing I need from this primitive is the
Yes. One very interesting structure you can build with this is a lock-free linked list (actually, this is the basis for the recursive split-order hash set 😃), Essentially, instead of physically removing nodes from a linked list, you logically remove a node by settings its
I like that. Maybe we could do something like this to give it some context to the user: class AtomicMarkableReferenceState < Struct.new(:value, :marked); end |
race: ack, thanks using ref = AtomicReference.new
ref.compare_and_set([old_val, old_mark], [new_val, new_mark])
use-case: Thanks for the example, lock-free linked list would be very useful too. struct: You may also want to look at https://github.com/ruby-concurrency/concurrent-ruby/blob/synchronization/lib/concurrent/synchronization/immutable_struct.rb. Normal Struct may have issues with visibility. |
ref = AtomicReference.new
ref.compare_and_set([old_val, old_mark], [new_val, new_mark]) I find this syntax somewhat counterintuitive and unidiomatic. I have never used a method in ruby with a signature similar to that. Althought I do like the idea of a ReferenceBooleanPair. I could possibly add support for that.
Nice. I will definitely use that. |
Although not germane to the conversation, this syntax: ref.compare_and_set([old_val, old_mark], [new_val, new_mark]) is very Erlang-ish ref:compare_and_set({old_val, old_mark}, {new_val, new_mark}) ->
{ok}. But, sadly, not very Ruby-ish. |
Sorry, I've actually meant it to be used only in the implementation of MarkableReference. It would not be exposed anywhere. Using the immutable struct it would look as follows: class MarkableReference < Synchronization::Object
Pair = ImmutableStruct.with_fields :value, :mark
def initialize(value, mark)
@Reference = AtomicReference.new Pair[value, mark]
ensure_ivar_visibility!
end
def compare_and_set(old_val, old_mark, new_val, new_mark)
@Reference.compare_and_set Pair[old_val, old_mark], Pair[new_val, new_mark]
end
#...
end |
@pitr-ch Unfortunately that creates the overhead of instantiating a struct object for every compare and set operation. Compare and set will be incredibly regular with the lock-free data structures, and it is unneccesary to do this much work to create a pair. Ideally, the compare_and_set can be accomplished with very little little logic (a bit mask for checking the mark, then a single XCHG assembly operation). Although I think for the pure ruby implementation using the SynchronizationObject should be perfect. |
Also @pitr-ch, would it be best that I implement it with Synchronization object once that synchronization branch gets merged into master? |
a2cae55
to
2f031f1
Compare
@ianks If the performance penalty of creating a new Struct on ever ref = Concurrent::AtomicMarkableReference.new(10)
value, mark = ref.get |
@ianks PS: Since returning multiple values from a Ruby method silently creates an array, there may be a performance hit there as well. We should run benchmark tests against both implementations. |
I did some benchmarks of the performance properties of instantiating different object types in different interpreters. Here is my methodology: https://gist.github.com/15362d1a28e24d57fe41
It seems across the board array is about 2-2.75x faster. rbx has a much slower struct implementation as of now, so I think that might rule out using a struct is this case. However the difference between class and array is somewhat small, and if it increased the usability enough I think it could warrant the performance hit. Although if performance is more important here, array might be the best way to go. UPDATE: It seems RBX has fixed a bug in their struct impl as a result of these benchmarks; however it is still around 6x slower. |
You might want to report that performance to Rubinius - an order of magnitude slower has got to be a bug and I'm sure |
👍 for array, I was advising the immutable struct because it ensures the visibility of its fields, not realizing that it is not necessary since read/write to atomic reference is volatile on all implementations. I am reasonably sure about MRI and JRuby. I am assuming Rbx too. @ianks it seems you know C/C++, you could verify: https://github.com/rubinius/rubinius/blob/master/vm/builtin/atomic.cpp#L23-L32, https://github.com/rubinius/rubinius/blob/b8b8cc4dcfd744e12c1360fab5fe4c66756febd7/vm/util/atomic.hpp#L95-L113 |
99f74fa
to
46e3b6e
Compare
I updated the implementation to rely on reference pairs (value, boolean) instead on locks for AtomicMarkableReference. @pitr-ch There was a mention in the SynchonizationObject doc about have fields which are auotmatically AtomicReferences, but I didn't have enough information to make that work on this go. If it is possible to do something like that, let me know so this can be more in line with the vision on the SynchronizationObject API. |
@@ -0,0 +1,152 @@ | |||
module Edge | |||
module Concurrent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be the other way around, Concurrent::Edge::AtomicMarkableReference
I was originally considering to add something like |
e83229b
to
6f058e9
Compare
Hmm, the tests are failing on JRuby for some reason. The interpreter is expecting 0 args for AtomicMarkableReference, when I have it defined as accepting 2 args with default values. Also, as a side note to save some time, the bug exists by using the constant
I imagine this could be a JRuby bug, or maybe an issue with synchronization object? Has anyone experienced this before? |
I'm in Detroit attending self.conference so I won't be able to look at this for a couple of days. I'll give you feedback as soon as I can. |
fail ::Concurrent::ConcurrentUpdateError, | ||
'AtomicMarkableReference: Update failed due to race condition.', | ||
'Note: If you would like to guarantee an update, please use ' \ | ||
'the `AtomicMarkableReference#update` method.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you also add API when it will just return false, raising and catching exception can expensive.
Looks good. 👍 Thanks for working on this. One idea though if you find it interesting. It seems to me that this class is a special case of more abstract class, let's name it |
Thanks @pitr-ch! Could you take a look at the issue I was having with the JRuby error? You can see the failures on Travis. I am thinking of reporting it as a bug to JRuby, but I think it may be an issue with Synchronization Object. |
22c7e65
to
af13ba8
Compare
class AtomicMarkableReference < ::Concurrent::Synchronization::Object | ||
# @!macro [attach] atomic_markable_reference_method_initialize | ||
def initialize(value = nil, mark = false) | ||
super |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
regarding the error, this should be super()
so the args are not passed to parent initialize. I've mistakenly left old initialize(*args, &block)
signature in ruby files (java extension has correct signature so it fails) so it was silently swallowing the args not using them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 48bacc4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pitr-ch let me know when that makes it into master so I can rebase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you do not need to wait for the fix to land in master, it can be merged earlier. The currently failing job is something else.
@pitr-ch I added the API for |
yeah something like |
This commit adds a pure Ruby implementation of an AtomicMarkableReference. An AtomicMarkableReference is much like an AtomicReference with that added property that it can be, you guessed it, marked either true or false. They are useful for implementing more advanced data sctructures such as recursive split-ordered hash tables, etc.
Objects should always be compared by reference so we can ensure that we are referring to the *same* object, not simply one that maybe be considered logically equivalent by overriding `==`. This is, of course, unless it is a Numeric. When comparing something primitive like a number, we expect that the equality operator makes mathematical sense.
In order to avoid raising errors (which are costly to performance), add an API for attempting an update which simply returns nil to the caller.
98a972e
to
916e92a
Compare
@pitr-ch I updated the |
Any idea why I am getting this error on JRuby?
|
Because that method doesn't exist on JRuby. The public method supported on all platforms is |
916e92a
to
6f663b3
Compare
Ok, Travis is happy now. Coveralls is angry though 😢 Let me know if there is anything more that needs to be done here. |
AtomicMarkableReference: Add pure Ruby implementation
This is the beginning of adding the primitive concurrent type AtomicMarkableReference. It is much like the AtomicReference, but contains a boolean
marked
value. It will be neccesary for implementing the recursive split-order (lock-free) hash table I plan to implement.The code is pretty straight forward, and steals mostly from AtomicReference; there are some refactorings possible there but I did not feel comfortable diffing too much code to make that happen on my first PR.
Some notes/question about the design:
get
returns an array of[@value, @marked]
, I am not a huge fan of returning an array like that for a public facing API, but since this is low level enough it might be justified. Plus it is rather easy to destructure ->markable_ref.get = val, mark
. Do you think returning an array here is idiomatic-enough Ruby? Obviously, returning the acutal object would be the most OO, but there are obvious race conditions there so maybe we are an exception to the rule here. An alternative would be to create some sort ofState
object which represents the given state of the object at the time of return. Thoughts?atomic_markable_reference_spec
attempted to create possible race conditions, etc; but there are specs in other places which rely on AtomicReference to work. Is this the way you guys have been testing the implementation in a threaded environment? Testing multithreaded implmentations is tough, as tests tend to fall short of covering the cases. Do you guys think I should add some tests to document in a threaded environment? Or would some sort of informal proof be better? Sorry about ramblng here; but if you guys could give me some better insight about testing in this lib in general I would be super appreciative!Please go crazy on the feedback for improvments here! I would love to optimize this code in any way possible 😃