Add core.experimental.refcount (continuation of #2608)#2679
Add core.experimental.refcount (continuation of #2608)#2679lesderid wants to merge 10 commits intodlang:masterfrom
Conversation
Squashed commit of the following: commit 56e1140 Author: Eduard Staniloiu <edi33416@gmail.com> Date: Thu Jun 13 15:05:36 2019 +0300 Remove dead code commit b7b8a02 Author: Eduard Staniloiu <edi33416@gmail.com> Date: Tue Jun 4 15:46:32 2019 +0300 Use shared type for refcount commit 95bb942 Author: Eduard Staniloiu <edi33416@gmail.com> Date: Tue May 28 17:06:49 2019 +0300 Use factory function; remove std import commit 30cc23b Author: Eduard Staniloiu <edi33416@gmail.com> Date: Tue May 28 16:45:20 2019 +0300 Add threading test commit 21474ed Author: Eduard Staniloiu <edi33416@gmail.com> Date: Tue May 28 14:45:34 2019 +0300 Add support for shared qualifier commit 3fa96c2 Author: Eduard Staniloiu <edi33416@gmail.com> Date: Tue May 21 18:37:26 2019 +0300 Rename to __RefCount commit edf7c31 Author: Eduard Staniloiu <edi33416@gmail.com> Date: Tue May 21 17:05:57 2019 +0300 Add ddoc commit 7a91e19 Author: Eduard Staniloiu <edi33416@gmail.com> Date: Tue May 21 14:35:34 2019 +0300 Fix style commit 9359780 Author: Eduard Staniloiu <edi33416@gmail.com> Date: Tue May 21 14:33:24 2019 +0300 isUnique is false if uninitialized commit ec62500 Author: Eduard Staniloiu <edi33416@gmail.com> Date: Tue May 21 14:25:28 2019 +0300 Remove superfluous const attr from ctor commit 02c04c9 Author: Eduard Staniloiu <edi33416@gmail.com> Date: Tue May 21 14:21:05 2019 +0300 Refactor test function commit 3a7e157 Author: Eduard Staniloiu <edi33416@gmail.com> Date: Tue May 21 14:00:41 2019 +0300 Rename to _RefCount and place at topfile commit d77baa2 Author: Eduard Staniloiu <edi33416@gmail.com> Date: Mon May 20 18:50:04 2019 +0300 Mark opAssign as return; return this commit fbaf79e Author: Eduard Staniloiu <edi33416@gmail.com> Date: Wed May 15 19:41:49 2019 +0300 Refactor commit 5c2883f Author: Eduard Staniloiu <edi33416@gmail.com> Date: Wed May 15 19:14:05 2019 +0300 Add RefCount struct
The functionality being tested is already covered by unit tests.
|
Thanks for your pull request and interest in making D better, @lesderid! We are looking forward to reviewing it, and you should be hearing from a maintainer soon.
Please see CONTRIBUTING.md for more information. If you have addressed all reviews or aren't sure how to proceed, don't hesitate to ping us with a simple comment. Bugzilla referencesYour PR doesn't reference any Bugzilla issue. If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog. Testing this PR locallyIf you don't have a local development environment setup, you can use Digger to test this PR: dub fetch digger
dub run digger -- build "master + druntime#2679" |
| // function is implicitly convertible to `immutable`. | ||
|
|
||
| shared CounterType* support = cast(shared CounterType*) pureAllocate(CounterType.sizeof); | ||
| *support = 1; // Start with 1 to avoid calling an `addRef` from 0 to 1 |
There was a problem hiding this comment.
This will fail to compile with the new (not yet implemented) restricted shared, please do `*cast()support = 1;.
There was a problem hiding this comment.
I changed shared CounterType* to shared(CounterType)*, as it's the counter that should be shared, not the pointer to it (as @thewilsonator pointed out in private). This makes this line compile with -preview=restrictiveshared (dlang/dmd#10142), but that seems like a bug in the restrictive shared implementation to me?
There was a problem hiding this comment.
I think so, probably best to change it anyway.
There was a problem hiding this comment.
This makes this line compile with
-preview=restrictiveshared
No it doesn't... shared(int) can't be assigned.
There was a problem hiding this comment.
Perhaps *cast(CounterType*)support = 1?
There was a problem hiding this comment.
We should really have cast(ref CounterType)lvalue to perform lvalue casting...
|
|
||
| Implementation: The internal implementation of `__RefCount` uses `malloc`/`free`. | ||
|
|
||
| Important: The `__RefCount` member must be initialized through a call to its |
There was a problem hiding this comment.
Can't we just disable the default initialization to enforce this?
@disable this();There was a problem hiding this comment.
This breaks rcarray for some reason: src/core/experimental/array.d(676): Error: variable `core.experimental.array.rcarray!int.rcarray.opSlice!(immutable(rcarray!int)).opSlice.__copytmp2437` default construction is disabled for type immutable(__RefCount)
There was a problem hiding this comment.
Sorry, where is src/core/experimental/array.d? somewhere in your fork of druntime?
| // a2 is the last ref to rcarray(4242) -> gets freed | ||
| } | ||
| assert(a.rc.isUnique); | ||
| } |
There was a problem hiding this comment.
Can we add a few more test to increase the coverage of this module and ensure it works as expected?
Maybe even one with multiple threads to ensure the atomic ops work too.
There was a problem hiding this comment.
Can we add a few more test to increase the coverage of this module and ensure it works as expected?
A bunch of tests were deleted with this commit
Maybe even one with multiple threads to ensure the atomic ops work too.
There was one which got deleted as well.
|
|
||
| License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). | ||
|
|
||
| Authors: Eduard Staniloiu |
There was a problem hiding this comment.
@lesderid please feel free to add your name here.
| private shared CounterType* rc = null; | ||
|
|
||
| /* | ||
| Perform `rc op val` operation. Always use atomics as the counter is shared. |
There was a problem hiding this comment.
Hmm, I guess this is the most controversial part of this PR as this will make it very slow for single-threaded programs.
Can't we separate the overloads, s.t. this is only done when __RefCount is actually shared (and thus its shared functions are called?).
Alternatively, maybe we need to add a __TLSRefCount?
There was a problem hiding this comment.
Sadly, we can't. With implicit conversion of immutable(__RefCount) to const(__RefCount) we can't rely on this being shared.
The solution @edi33416 was trying out for this used alignment as a marker for __RefCounts that were constructed as shared (e.g. immutable), so after implicit conversion to const we can still know it was initially immutable.
However this doesn't work either, as the result of pure functions can be implicitly converted to immutable. This means that when creating and returning a __RefCount from a pure function, the non-shared constructor ends up being called for a shared object, meaning it gets constructed with non-shared alignment, and we're right back at square one.
There was a problem hiding this comment.
I have a whole lot of problems with this conversation. It seems like there are mistakes everywhere.
There was a problem hiding this comment.
@TurkeyMan Any insights you might have about this particular issue would be very much appreciated.
There was a problem hiding this comment.
I haven't had time to think on it sorry, as funny as it might seem to some, RC is not actually an urgent issue for me as it has been for the last 10 years >_<
This is very important, but I need to devote 100% attention to shared right now.
There was a problem hiding this comment.
My instinct is that RC is so important, that immutable->shared conversion causing us an issue here is satisfactory grounds to re-consider immutable->shared conversion rules... the problem might not be here, maybe it's there. (I don't know, I haven't thought on it at all, that's just my feeling)
There was a problem hiding this comment.
I've had the same thought. I think it's definitely worth looking into.
| unittest | ||
| { | ||
| auto rc = __RefCount.make(); | ||
| assert(rc.isUnique); |
There was a problem hiding this comment.
You talk about atomic rc, but then there's isUnique? They're incompatible.
| import core.atomic : atomicOp; | ||
|
|
||
| alias CounterType = uint; | ||
| private shared(CounterType)* rc = null; |
There was a problem hiding this comment.
No offense personally, but this is the most disappointing implementation of reference counting that is possible to write. We can do better than this, and if we can't, then we have serious language issues.
| @nogc nothrow pure @trusted scope | ||
| private shared(CounterType) rcOp(this Q, string op)(CounterType val) const | ||
| { | ||
| return cast(shared(CounterType)) (atomicOp!op(*(cast(shared(CounterType)*) rc), val)); |
There was a problem hiding this comment.
How does this work? rc is const, yet you mutate it... D's const is transitive, and D doesn't have mutable or anything like it, and the optimiser is free to do whatever it likes with the knowledge a value is const.
I don't think this line can go in the core runtime...?
There was a problem hiding this comment.
Also, where's the memory order argument? It looks like it would execute this seq-cst (assuming that's the default), which is wrong.
| This increases the reference count. | ||
| */ | ||
| @nogc nothrow pure @safe scope | ||
| this(return scope ref typeof(this) rhs) |
There was a problem hiding this comment.
Why typeof(this) and not __RefCount?
There was a problem hiding this comment.
It's idiomatic D to not repeat the struct name and reference it via typeof.
There was a problem hiding this comment.
Where's that written? I've seen Walter recommending against typeof(this) used in this way on numerous occasions.
There was a problem hiding this comment.
Not a good place to find great D code ;)
| rc = cast(typeof(rc)) support; | ||
| } | ||
|
|
||
| private enum copyCtorIncRef = q{ |
There was a problem hiding this comment.
Please don't do this, it's not debug-able.
There was a problem hiding this comment.
What would be the alternative?
There was a problem hiding this comment.
Does a mixin template work here? If not, just repeat the code; its only 3 lines.
Why does it need to be repeated so many times anyway? Can inout be used to reduce duplication?
There was a problem hiding this comment.
It does use inout?
There was a problem hiding this comment.
Oh yeah, sorry I didn't see it in the sea of attributes. I was expecting it attributed to the context.
Can a constructor be inout? Wouldn't that cut the number of ctors down to 2; one inout and one inout shared?
|
|
||
| private enum copyCtorIncRef = q{ | ||
| rc = rhs.rc; | ||
| assert(rc == rhs.rc); |
There was a problem hiding this comment.
What kind of failure is this assert trying to catch?
|
An interesting observation to me, is that this seems like a whole lot more code (maybe 3-4 times as much code) as the same functionality would be in C++, and that's even considering that we're using those ugly text mixins everywhere to reduce the code bloat... I think it's possible that D is still lacking necessary foundation for this :/ Since this solution is functionally identical to C++, we should expect it to take the same amount of lines, or less. |
| $(BIGOH 1). | ||
| */ | ||
| pure nothrow @safe @nogc scope | ||
| bool isUnique(this Q)() const |
There was a problem hiding this comment.
This function must be removed. I can't imagine anything you could do with this that's not a data race.
There was a problem hiding this comment.
Removing the call to isInitialized will remove the data race at the cost of generating a segfault for an invalid (uninitialized) __RefCounted instance.
This function provides a simple interface towards the user to help him decide if he needs to deallocate the internal state of his data structure.
An "alternative" to this would be to expose the internal delRef to the user, which could break / invalidate the automatic reference counting. The user should never be able to explicitly modify the RC, but only "view" the value.
I propose that we bite the bullet and take the segfault.
There was a problem hiding this comment.
If the RC is atomic, then none of those things you say can exist. Everything you just said is a data race.
If RC is thread-local (as it should be in D), then we have options.
|
I can't review this properly right now. Please hold off on merging until I had a proper look. Which I'll signal by approving this pr. |
|
Yes, this shouldn't be merged until we've been able to address the issues Manu pointed out. (Having a feature to turn PRs into draft PRs would be very useful for cases like this, so it could be helpful to give that suggestion some support here: https://github.bokerqi.topmunity/t5/H/F/m-p/19107) |
|
We're trying out a possible alternative, a shared pointer type: #2690. |
|
You can have local counter, if you share immutable data in a shared container that would provide local rc wrappers for threads. When counter reaches zero you can see if it came from a shared container. See also this. |
|
RC requires __mutable to be implemented. There is a reference to this PR here: https://issues.dlang.org/show_bug.cgi?id=23071 |
Continuation of #2608
This PR adds a struct
__RefCountthat can be used as a common building block for reference counted types through composition.A short summary of the known issues that were brought up in the previous thread and earlier discussions:
__because the compiler might need to be made aware of its existence in the future (e.g. to inhibit elision of calls toconst purefunctions likeaddRef/delRef)purefunctions toimmutable(andimmutableimplyingshared), we have to conservatively use atomic increments/decrements for the counter variableNone of these issues are bad enough to keep blocking progress. Early inclusion in
core.experimentalallows us to experiment with reference counted containers using this reference counting scheme (e.g.rcarray!T, #2646) while not yet fully committing to a specific implementation or API.