Feature #19406
closedAllow declarative reference definition for rb_typed_data_struct
Description
Summary¶
This PR proposes an additional API for C extension authors to define wrapped
struct members that point to Ruby objects, when the struct being wrapped
contains only members with primitive types (ie. no arrays or unions). The new
interface passes an offset from the top of the data structure, rather than the
reference VALUE
itself, allowing the GC to manipulate both the reference edge
(the address holding the pointer), as well as the underlying object.
This allows Ruby's GC to handle marking, object movement and reference updating
independently without calling back into user supplied code.
Implementation¶
When a wrapped struct contains a simple list of members (such as the
struct enumerator
in enumerator.c
). We can declare all of the struct members that
may point to valid Ruby objects as RUBY_REF_EDGE
in a static array.
If we choose to do this, then we can mark the corresponding rb_data_type_t
as
RUBY_TYPED_DECL_MARKING
and pass a pointer to the references array in the
data
field.
To avoid having to also find space in the rb_data_type_t
to define a length for
the references list, I've chosen to require list termination
with RUBY_REF_END
- defined as UINTPTR_MAX
. My assumption is that no
single wrapped struct will ever be large enough that UINTPTR_MAX
is actually a
valid reference.
We don't have to then define dmark
or dcompact
callback functions. Marking,
object movement, and reference updating will be handled for us by the GC.
struct enumerator {
VALUE obj;
ID meth;
VALUE args;
VALUE fib;
VALUE dst;
VALUE lookahead;
VALUE feedvalue;
VALUE stop_exc;
VALUE size;
VALUE procs;
rb_enumerator_size_func *size_fn;
int kw_splat;
};
static const size_t enumerator_refs[] = {
RUBY_REF_EDGE(enumerator, obj),
RUBY_REF_EDGE(enumerator, args),
RUBY_REF_EDGE(enumerator, fib),
RUBY_REF_EDGE(enumerator, dst),
RUBY_REF_EDGE(enumerator, lookahead),
RUBY_REF_EDGE(enumerator, feedvalue),
RUBY_REF_EDGE(enumerator, stop_exc),
RUBY_REF_EDGE(enumerator, size),
RUBY_REF_EDGE(enumerator, procs),
RUBY_REF_END
};
static const rb_data_type_t enumerator_data_type = {
"enumerator",
{
NULL,
enumerator_free,
enumerator_memsize,
NULL,
},
0, (void *)enumerator_refs, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_DECL_MARKING
};
Benchmarking¶
Benchmarking shows that this reference declaration style does not degrade
performance when compared to the callback style.
To benchmark this we created a C extension that initialized a struct with 20
VALUE
members, all set to point to Ruby strings. We wrapped each struct using
TypedData_Make_Struct
in an object. One object was configured with callback
functions and one was configured with declarative references.
In separate scripts we then created 500,000 of these objects, added them to a
list, so they would be marked and not swept and used
GC.verify_compaction_references
to make sure everything that could move, did.
Finally we created a wrapper script that used seperate processes to run each GC
type (to ensure that the GC's were completely independent), ran each benchmark
50 times, and collected the results of GC.stat[:time]
.
We did this on an M1 Pro MacBook (aarch64), and a Ryzen 3600 We then plotted the
results:
As we can see from this, there has been no real impact to GC performance in our
benchmarks.
Benchmark code and harnesses is available in this Github
repo
Justification¶
Requiring extension authors to implement seperate dmark
and dcompact
callbacks can be error-prone, and pushes GC responsibilities from the GC into
user supplied code. This can be a source of bugs arising from the dmark
and
dcompact
functions being implemented incorrectly, or becoming out of sync with each other.
There has already been work done by @peterzhu2118 (Peter Zhu) to try and unify these
callbacks, so that authors can define a
single function, that will be used for both marking and compacting, removing the
risk of these callbacks becoming out of sync.
This proposal works alongside Peter's earlier work to eliminate the
callbacks entirely for the "simple reference" case.
This means that extension authors with simple structs to wrap can declare which
of their struct members point to Ruby objects to get GC marking and compaction
support. And extension authors with more complex requirements will only have to
implement a single function, using Peter's work.
In addition to this, passing the GC the address of a reference rather than the
reference itself (edge based, rather than object based enqueing), allows the GC
itself to have more control over how it manipulates that reference.
This means that when considering alternative GC implementations for Ruby (such
as our ongoing work integrating MMTk into
Ruby1), We don't need to call from Ruby
into library code, and then back into Ruby code as often; which can increase
performance, and allow more complex algorithms to be implemented.
Trade-offs¶
This PR provides another method for defining references in C extensions, in
addition to the callback based approach, effectively widening the extension API.
Extension authors will now need to choose whether to use the declarative
approach, or a callback based approach depending on their use case. This is more
complex for extension authors.
However because the callback's do still exist, this does mean that extension
authors can migrate their own code to this new, faster approach at their
leisure.
Further work¶
As part of this work we inspected all uses of rb_data_type_t
in the Ruby
source code and of 134 separete instances, 60 wrapped structs that contained
VALUE
members that could point to Ruby objects. Out of these 27 were "simple"
structs that would benefit from this approach, 28 contained complex references
(unions, loops etc) that won't work with this approach, and 5 were situations
that were unsure, that we believe we could make work given some slight
refactors.
Updated by Hanmac (Hans Mackowiak) almost 2 years ago
i don't know if it works for my use case too, but for my c++ extension,
i need to keep the ruby object alive for as long as the C++ object lives.
I solved it with this approach:
- have a ruby Hash defined as global value so it doesn't get freed (this is for hiding my objects from the ruby GC)
- the C++ class has a Pointer for additional data that gets deleted when the C++ object is getting deleted.
- now when my Additional Data Object is deleted, the ruby object that is stored inside is removed from the global hash. (i use this as a hook)
- now the ruby object can be safely deleted by the Ruby GC
the important part is that the lifetime of the ruby object is colinked to the lifetime of the C++ object (from the c++ library point of few)
so i neither get a new ruby object for as long as the c++ lives,
and i also should not try to access the c++ object after it gets deleted but the ruby object is still alive.
(also deleting the ruby object should not delete the c++ object because its hanging in a framework)
Updated by nobu (Nobuyoshi Nakada) over 1 year ago
rb_random_interface_t
uses rb_data_type_struct::data
.
Updated by ko1 (Koichi Sasada) over 1 year ago
On dev-meeting there is no objection about the basic concept.
Trivial points.
- (matz) can we have better macro to define the memory layout (
enumerator_refs
)? - (nobu) we should not use
rb_data_type_struct::data
to specify the memory layout. Should we re-usedmark
field with theRUBY_TYPED_DECL_MARKING
flag?
Updated by eightbitraptor (Matt V-H) over 1 year ago
ko1 (Koichi Sasada) wrote in #note-3:
On dev-meeting there is no objection about the basic concept.
Great, thank you.
Trivial points.
- (matz) can we have better macro to define the memory layout (
enumerator_refs
)?
I tried this in a previous iteration (in fact I accidentally left a reference to the RUBY_REFERENCE_LIST
macro in the rdoc). The problem is that we require C++98 support for C extensions, so I am unable to use __VA_ARGS__
to declare arbitrary size reference lists.
I know there are ways to work around this in the pre-processor, but this would require us to have an upper limit on the number of references. Whatever limit we choose could end up being a problem for C extensions...
- (nobu) we should not use
rb_data_type_struct::data
to specify the memory layout. Should we re-usedmark
field with theRUBY_TYPED_DECL_MARKING
flag?
I've implemented this. A much better idea than using data
, thanks.
Updated by ko1 (Koichi Sasada) over 1 year ago
eightbitraptor (Matthew Valentine-House) wrote in #note-4:
- (matz) can we have better macro to define the memory layout (
enumerator_refs
)?I tried this in a previous iteration (in fact I accidentally left a reference to the
RUBY_REFERENCE_LIST
macro in the rdoc). The problem is that we require C++98 support for C extensions, so I am unable to use__VA_ARGS__
to declare arbitrary size reference lists.I know there are ways to work around this in the pre-processor, but this would require us to have an upper limit on the number of references. Whatever limit we choose could end up being a problem for C extensions...
On the dev-meeting, there were some comments about that:
(1) interpreter core doesn't need to care about C++ compilers.
(2) should we care about old C++ compilers for newly created C-extensions?
anyway, name better macro names can improve the issue.
Updated by eightbitraptor (Matt V-H) over 1 year ago
- Status changed from Open to Closed
Applied in changeset git|7142328a94c85cc5f23630396f248b32133f87ab.
[Feature #19406] Allow declarative definition of references
When using rb_data_type_struct to wrap a C struct, that C struct can
contain VALUE references to other Ruby objects.
If this is the case then one must also define dmark and optionally
dcompact callbacks in order to allow these objects to be correctly
handled by the GC. This is suboptimal as it requires GC related logic to
be implemented by extension developers. This can be a cause of subtle
bugs when references are not marked of updated correctly inside these
callbacks.
This commit provides an alternative approach, useful in the simple case
where the C struct contains VALUE members (ie. there isn't any
conditional logic, or data structure manipulation required to traverse
these references).
In this case references can be defined using a declarative syntax
as a list of edges (or, pointers to references).
A flag can be set on the rb_data_type_struct to notify the GC that
declarative references are being used, and a list of those references
can be assigned to the dmark pointer instead of a function callback, on
the rb_data_type_struct.
Macros are also provided for simple declaration of the reference list,
and building edges.
To avoid having to also find space in the struct to define a length for
the references list, I've chosed to always terminate the references list
with RUBY_REF_END - defined as UINTPTR_MAX. My assumption is that no
single struct will ever be large enough that UINTPTR_MAX is actually a
valid reference.