Skip to content
This repository was archived by the owner on Oct 12, 2022. It is now read-only.
/ druntime Public archive

Comments

Add core.experimental.refcount (continuation of #2608)#2679

Closed
lesderid wants to merge 10 commits intodlang:masterfrom
lesderid:refcount
Closed

Add core.experimental.refcount (continuation of #2608)#2679
lesderid wants to merge 10 commits intodlang:masterfrom
lesderid:refcount

Conversation

@lesderid
Copy link
Contributor

Continuation of #2608

This PR adds a struct __RefCount that 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:

  1. The type needs to be prefixed with __ because the compiler might need to be made aware of its existence in the future (e.g. to inhibit elision of calls to const pure functions like addRef/delRef)
  2. It's a non-intrusive approach, i.e. it doesn't use space lent by the object it's tracking, which means worse locality of reference and more (de)allocations
  3. Because of implicit conversion of return values of pure functions to immutable (and immutable implying shared), we have to conservatively use atomic increments/decrements for the counter variable

None of these issues are bad enough to keep blocking progress. Early inclusion in core.experimental allows 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.

edi33416 and others added 4 commits July 16, 2019 01:51
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.
@dlang-bot
Copy link
Contributor

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 verify that your PR follows this checklist:

  • My PR is fully covered with tests (you can see the coverage diff by visiting the details link of the codecov check)
  • My PR is as minimal as possible (smaller, focused PRs are easier to review than big ones)
  • I have provided a detailed rationale explaining my changes
  • New or modified functions have Ddoc comments (with Params: and Returns:)

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 references

Your 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 locally

If 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"

@lesderid lesderid changed the title Add core.experimental.__RefCount (continuation of #2608) Add core.experimental.refcount (continuation of #2608) Jul 16, 2019
// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fail to compile with the new (not yet implemented) restricted shared, please do `*cast()support = 1;.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, probably best to change it anyway.

Copy link
Contributor

@TurkeyMan TurkeyMan Jul 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes this line compile with -preview=restrictiveshared

No it doesn't... shared(int) can't be assigned.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps *cast(CounterType*)support = 1?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just disable the default initialization to enforce this?

@disable this();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, where is src/core/experimental/array.d? somewhere in your fork of druntime?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's here, sorry: #2646

// a2 is the last ref to rcarray(4242) -> gets freed
}
assert(a.rc.isUnique);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor Author

@lesderid lesderid Jul 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a whole lot of problems with this conversation. It seems like there are mistakes everywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TurkeyMan Any insights you might have about this particular issue would be very much appreciated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had the same thought. I think it's definitely worth looking into.

unittest
{
auto rc = __RefCount.make();
assert(rc.isUnique);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why typeof(this) and not __RefCount?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's idiomatic D to not repeat the struct name and reference it via typeof.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where's that written? I've seen Walter recommending against typeof(this) used in this way on numerous occasions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Phobos source code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a good place to find great D code ;)

rc = cast(typeof(rc)) support;
}

private enum copyCtorIncRef = q{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't do this, it's not debug-able.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the alternative?

Copy link
Contributor

@TurkeyMan TurkeyMan Jul 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does use inout?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of failure is this assert trying to catch?

@TurkeyMan
Copy link
Contributor

TurkeyMan commented Jul 19, 2019

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function must be removed. I can't imagine anything you could do with this that's not a data race.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@UplinkCoder
Copy link
Member

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.

@lesderid
Copy link
Contributor Author

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)

@lesderid
Copy link
Contributor Author

We're trying out a possible alternative, a shared pointer type: #2690.

@anon17
Copy link

anon17 commented Jul 24, 2019

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.

@dlang-bot dlang-bot added Needs Rebase needs a `git rebase` performed stalled labels May 27, 2021
@RazvanN7
Copy link
Contributor

RC requires __mutable to be implemented. There is a reference to this PR here: https://issues.dlang.org/show_bug.cgi?id=23071

@RazvanN7 RazvanN7 closed this Apr 28, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Needs Rebase needs a `git rebase` performed stalled

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants