atomic record fields: implement [%atomic.field ...]#13707
Conversation
This is a breaking change because this function was (unfortunately) exposed outside CAML_INTERNALS, and is used by exactly one external user, you guessed it: https://github.com/ocaml-multicore/multicore-magic/blob/360c2e829c9addeca9ccaee1c71f4ad36bb14a79/src/Multicore_magic.mli#L181-L185 https://github.com/ocaml-multicore/multicore-magic/blob/360c2e829c9addeca9ccaee1c71f4ad36bb14a79/src/unboxed5/multicore_magic_atomic_array.ml#L36-L43 We chose to change the prototype to remain consistent with the naming convention for the new caml_atomic_*_field primitives, which will be added to support atomic record fields. User code can easily adapt to this new prototype we are using, but not in a way that is compatible with both old and new versions of OCaml (not without some preprocessing at least). Another option would be to expose int caml_atomic_cas_field(value obj, intnat fld, value, value) value caml_atomic_cas_field_boxed(value obj, value vfld, value, value) but no other group of primitives in the runtime uses this _boxed terminology, they instead use int caml_atomic_cas_field_unboxed(value obj, intnat fld, value, value) value caml_atomic_cas_field(value obj, value vfld, value, value) and this would again break compatiblity -- it is not easier to convert code to that two-version proposal, and not noticeably more efficient. So in this case we decided to break compatibility (of an obscure, experimental, undocumented but exposed feature) in favor of consistency and simplificity of the result.
…_exchange_field] and [caml_atomic_fetch_add_field].
Uses of existing atomic primitives %atomic_foo, which act on
single-field references, are now translated into %atomic_foo_field,
which act on a pointer and an offset -- passed as separate arguments.
In particular, note that the arity of the internal Lambda primitive
Patomic_load
increases by one with this patchset. (Initially we renamed it into
Patomic_load_field
but this creates a lot of churn for no clear benefits.)
We also support primitives of the form %atomic_foo_loc, which
expects a pair of a pointer and an offset (as a single argument),
as we proposed in the RFC on atomic fields
ocaml/RFCs#39
(but there is no language-level support for atomic record fields yet)
Co-authored-by: Clément Allain <clef-men@orange.fr>
Requires a bootstrap. Co-authored-by: Gabriel Scherer <gabriel.scherer@gmail.com>
This type will be used for ['a Atomic.Loc.t], as proposed in the RFC ocaml/RFCs#39 We implement this here to be able to use it in the stdlib later, after a bootstrap.
We want to use [mark_label_used] in a context where we cannot easily find the label declaration, only the label description (from the environment).
This bootstrap is not required by a compiler change, but it enables the use of the predefined type `'a atomic_loc` and the expression-former [%atomic.loc ...] in the standard library.
|
Maybe we could discuss the treatment of inline records again. In the above code, when we match with the pattern |
|
While discussing an optimisation that could be made for what I'll call "heterogenous arrays": type _ field = A : int field | B : float field | C : string field
type drecord = { mutable a : int ; mutable b : float ; mutable c : string }
let[@inline] ( .!() ) (type a) r (f : a field) : a =
match f with
| A -> r.a
| B -> r.b
| C -> r.c
let[@inline] ( .!()<- ) (type a) r (f : a field) (v : a) =
match f with
| A -> r.a <- v
| B -> r.b <- v
| C -> r.c <- vwhere the accessed could be simplified to direct access instead of using a switch, @lthls remarked that the type drecord = { mutable a : int ; mutable b : float ; mutable c : string }
type 'a field = (drecord, 'a) Field.t
let a : int field = [%field a]
let b : float field = [%field b]
let c : string field = [%field c]
let[@inline] ( .!() ) (type a) r (f : a field) : a = Field.get r f
let[@inline] ( .!()<- ) (type a) r (f : a field) (v : a) = Field.set r f vIt's not related to atomics at all, but if we figure out the issues related to inline records (which I believe is ongoing), it is an example where it would be beneficial to extend the |
|
I believe this optimization could be useful in We discussed it with @gasche some time ago. As far as I remember, we were thinking of performing it in the compiler. |
|
Yes, I think it wouldn't be too hard to extend the affine-switch-detection pass to deal with integer constants under a few (hardcoded) contexts of interest, here applications of P{get,set}field. But I agree that doing it in the language is also nice... if we can manage to stay on the razor-thin consensual side of the first-class-field debate. |
|
I assume we can close this now that #13404 has been merged. |
This PR is an alternative to #13404 which implements an alternative design that was also mentioned in RFC #39. It is currently built on top of #13404, so it implements both
[%atomic.loc foo.count](as #13404) and[%atomic.field count](new in the present PR). I marked it as a "draft" because, if we decided that[%atomic.field count]is the better design, we would probably remove support for[%atomic.loc foo.count]before merging.Summary of previous episodes
Consider a record type declaration with a field annotation as atomic:
The atomic record fields RFC (#39) initially proposed a new construction
[%atomic.field count], which would have type(t, int) Atomic.Field.t, and whose runtime representation would be the integer 0 (the offset of the record field). One difficulty we noted in the RFC is that this has to support type-based disambiguation of record fields.Then @bclement-ocp made a simplifying proposal, which looks like
[%atomic.loc x.count](whenxhas typet), at typeint Atomic.Loc.t. The runtime representation is the pair(x, 0), but the pair is optimized away if an atomic primitive is directly applied to this value. This is simpler because the magic typeAtomic.Foo.ttakes a single type parameter, and there is no need for new/specific label disambiguation code, we can reuse the exact same logic as field access.This proposal was implemented by @clef-men in #13404, and reviewed by @OlivierNicole. When we discussed it at the last developer meeting, people asked if we really want the
[%atomic.loc x.count]version and if we should consider[%atomic.field count]instead, for the following reasons:After positive feedback on this suggestion from @polytypic, we decided to explore the
[%atomic.field ...]route. The present PR is the result of my exploration.Bad interactions between
%atomic.fieldand inline record fields.My conclusion after implementing this feature is that
[%atomic.field ...]does not work well for atomic fields in inline records. I consider this a serious blocker because inline records are being used in concurrent OCaml code that would benefit from atomic fields.The problem comes from an intentional limitation of OCaml support for inline records: consider
In this example,
Filled rhas typeu, so the type ofris the inline record type ofFilled. This type is pretty-printed asu.Filledby the type-checker, but:rby preventing any use ofrother than direct field accessDue to restriction (1), we cannot explicitly disambiguate all
%atomic.fieldexpressions with a type annotation, in this example for example we cannot write([%atomic.field amount] : (u.Filled, _) Atomic.Field.t), so there are situations where it will be impossible for the user to correctly indicate which of the several record fieldsamountthey mean.I thought (see my "hunch" in ocaml/RFCs#39) that this would be okay in practice because in the cases where the
%atomic.fieldexpression is directly applied to an atomic primitive, as is the case in this example, bidirectional propagation of argument types should do the trick:Atomic.Field.fetch_and_addhas type'r -> ('r, int) Atomic.Field.t -> int -> int, so the type ofr(the first argument offetch_and_addshould be propagated) into the expected type of the second argument, allowing disambiguation.Unfortunately this does not work as expected, due to restriction (2) the use of
rin argument position is forbidden by the type-checking code for variables of inline record type, and this example fails with the following error:My conclusion is that
%atomic.fieldis not usable with inline records, unless we consider significant changes to the type-checking of inline records.Where to go from there?
I see three reasonable options going forward:
Give up on
%atomic.fieldand merge Atomic record fields #13404 which implements the simpler%atomic.loc, has been morally approved and carefully reviewed.Merge something like the current state of the present PR, with a fully working
%atomic.locand additionally a version of%atomic.fieldthat has advantages but does not work with inline records. Later we can consider changing the type-checking of inline records to lift this limitation, and we would end up with two features that work well. (And some duplication as each atomic primitivefoowould be available asAtomic.Field.fooand alsoAtomic.Loc.foo, but there are 5 of them so it's okay.)Decide that we absolutely want
%atomic.fieldand not%atomic.loc, and that we must first change the type-checking of inline records to lift the current limitations of%atomic.field.To improve the type-checking of inline records, I think we could do one of the following:
A. give up on the idea that inline record types cannot be named, make
u.Filledactual syntax, and lift all limitations on those types, orB. instead of the current heavy-handed approach to preventing the escape of inline record types, handle them like existential types (scoping them at the current level), so that mentioning them in the result type of the expression is forbidden but passing them to arbitrary functions inside the expression is fine.