-
Notifications
You must be signed in to change notification settings - Fork 13
Complete redesign of the kcas library API #22
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
Conversation
e82c756 to
8352001
Compare
1602567 to
5f785bd
Compare
d016e86 to
822abf3
Compare
34ddbe7 to
6150f30
Compare
|
The However, there might be practical ways to extend the To support blocking, one essentially needs a way to signal waiters. After mutating some locations the mutator signals waiters. For a scalable mechanism that signal needs to be selective and only wake up those waiters that are interested in the mutated locations. To associate waiters with locations in a truly low-overhead fashion, one possibility would be to allow locations to be "tagged": module Loc : sig
type ('tag, 'a) tagged
val make_tagged: 'tag -> 'a -> ('tag, 'a) tagged
val get_tag : ('tag, 'a) tagged -> 'tag
type 'a t = (unit, 'a) t
(* ... *)In a blocking transaction mechanism that Additionally, a scalable blocking mechanism also needs to be able to efficiently figure out which locations have been read and which have been written. A waiter needs to add itself to the read locations and a mutator needs to signal waiters of written locations. module Tx : sig
(* ... *)
module Log : sig
type t
type 'r reducer = {
one : 't 'a. ('t, 'a) Loc.tagged -> 'r;
zero : 'r;
plus : 'r -> 'r -> 'r;
}
val reduce : 'r reducer -> t -> 'r
(** [reduce reducer] performs a fold over the transaction log. *)
end
exception Retry of unit t
(** Exception raised by {!reset_and_retry}. *)
val reset_and_retry : (Log.t -> unit t) -> 'a t
(** [reset_and_retry on_read] returns a transaction that resets the current
transaction such that it only reads from the accessed locations. The
[on_read] function is then called with the internal transaction log to
construct a transaction that is then composed after the current
transaction. The composed transaction [tx] is then raised as a
[Retry tx] exception. *)
val written: (Log.t -> unit t) -> 'a t -> 'a t
(** [written on_written tx] returns a transaction that executes as [tx] and
then calls the given function with a view of the internal transaction log
restricted to the written locations. The returned transaction is then
composed after the transaction [tx].
The intended use case for [written] is to extend a transaction to signal
waiters in a blocking transaction mechanism:
{[
let rec blocking_tx tx =
let all_waiters = Loc.make [] in
match
tx
|> written (fun log ->
(* remove all waiters of all written locations
and add them to the [all_waiters] list. *)
)
|> attempt
with
| result ->
(* signal [all_waiters] *)
result
| exception Exit ->
blocking_tx tx
| exception Retry add_waiters_tx -> (
match attempt add_waiters_tx with
| () ->
(* blocking wait *)
blocking_tx tx
| exception Exit ->
(* Locations were already mutated before waiters could be added *)
blocking_tx tx)
]} *)The idea of (resetting and) extended transactions with the waiter operations is that this way the kcas mechanism itself checks whether the waiters should be added (as the read locations didn't change during the original transaction and the addition of waiters — if either fails then the transaction can be just retried without blocking) or signaled (as the mutations, including taking all the waiters, were completed successfully). The above is only a preliminary idea. I have not yet fully implemented the above to verify it in practise. Here is the let rec blocking_tx tx =
let all_waiters = Loc.make [] in
match
tx
|> written (fun log ->
(* remove all waiters of all written locations
and add them to the [all_waiters] list. *)
)
|> attempt
with
| result ->
(* signal [all_waiters] *)
result
| exception Exit ->
blocking_tx tx
| exception Retry add_waiters_tx -> (
match attempt add_waiters_tx with
| () ->
(* blocking wait *)
blocking_tx tx
| exception Exit ->
(* Locations were already mutated before waiters could be added *)
blocking_tx tx)Of course, a proper implementation would be a bit more complicated with things like backoff. |
bartoszmodelski
left a comment
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.
Thanks for the PR. The new interface is a lot cleaner. I left a few comments with questions and minor suggestions.
I experimented a bit with transactions and they look solid to me.
| let create ?(lower_wait_log = 4) ?(upper_wait_log = 17) () = | ||
| assert ( | ||
| 0 <= lower_wait_log | ||
| && lower_wait_log <= upper_wait_log | ||
| && upper_wait_log <= max_wait_log); | ||
| (upper_wait_log lsl (bits * 2)) | ||
| lor (lower_wait_log lsl bits) lor lower_wait_log |
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.
Afaict there's no way in ocaml to pack this efficiently into a record or tuple of bytes, giving justification to this representation. Do you think it'd be useful to have a separate lib for packing multiple shorts into an int?
| executed by {!once}. *) | ||
|
|
||
| val default : t | ||
| (** [default] is equivalent to [create ()]. *) |
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.
Is this exposed for performance reasons?
|
BTW, one thing that I'd love to see is that we'd have documentation from different library versions simultaneously available in the gh-pages. I worked on a such thing for one of own projects the other weekend and I could prepare some scripts for kcas to do the same. |
|
This sounds good for API reference (with all other info being in the readme, eio-style). Do you have it up for the other project already? |
344c2e4 to
903ed8e
Compare
Yes and no. I was planning to use it e.g. in But the basic idea is very simple. The script first clones gh-pages branch and clears it completely (locally). It then iterates through all the git (branches or) tags, runs the commands to generate documentation, and if generation was successful, copies the generated documentation to the -https://ocaml-multicore.github.io/kcas/doc/
+https://ocaml-multicore.github.io/kcas/main/ |
bartoszmodelski
left a comment
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.
LGTM
df244b3 to
1b782f8
Compare
This PR proposes a complete redesign of the API that divides the API into four modules
Backoff,Loc,Op, andTx. The diffs show how to change client code to use the new API.As proposed in this PR,
Locis a module that has a signature that is essentially compatible with the signature of the stdlibAtomicmodule. The main difference is that several operations take an optionalbackoff(and that there is aBackoffmodule). Client code wishing to perform multiplecompare_and_setoperations atomically could theoretically just switch to useLocinstead ofAtomic.The constructor function
refis also renamed tomake.The name
ref, while not a keyword in OCaml, has a long history and it is probably better to avoid it. The term "location" is used e.g. in the papers on the OCaml memory model.The previous API contained two related operations named
try_mapandmap. I believe this is a mistake in a couple of ways. First of all, the mnemonicmapis generally used for functional updates whilekcasis fundamentally imperative. Second, themapoperations had arguably cumbersome signatures and the signature ofmapwas also imprecise. This PR proposes a single simplerupdateoperation that also requires fewer allocations. Note how in this PR several other operations are implemented in terms ofupdate.The previous API had a
W1module. That has been removed, because it didn't provide anything substantial beyond of what the stdlibAtomicalready provides.Actually the
W1module did providemapandtry_map. It would probably make sense to move theBackoffmodule to the stdlib and also to add anAtomic.updatefunction. I leave that to future work.This PR also includes a redesign of the
Backoffmodule to use an internal representation as a single immutableintand theonceoperation witht -> ttype. This wayBackoffdoes not require memory allocations during a backoff loop and possibility of false sharing is also reduced. The default lower and upper backoff values are chosen to approximate what is currently in thelockfreelibraryBackoffmodule.The atypically named
kCASoperation has been renamed toatomicallyandcommitis now calledatomic. They are now inside theOpmodule.This PR also adds an API as the
Txmodule for performing transactions on shared memory locations. One may consider the transaction API as a higher-level alternative to constructing a list of operations as with theOpmodule. Transactions can be composed sequentially and conditionally. This PR includes two different transactional queues and a stack and demonstrates that one can transfer elements between different data structures atomically.Here is an example of committing a transaction that swaps the values of the two shared memory references
x_locandy_locand returns their sum:One potentially interesting avenue for further work would be to extend the algorithms and this library to support efficient compare-only operations. One can express a kind of compare operation as
mk_cas loc expected expected. While that works, it writes to the location potentially causing contention and resulting in poor performance. Efficient read-only transactions, for example, could be a useful addition to kcas. As mentioned inoperation with multiple compares and a single swap also has uses. I'll leave this to further work.