Jump to page: 1 2 3
Thread overview
Why do immutable variables need reference counting?
Apr 10, 2022
norm
Apr 10, 2022
rikki cattermole
Apr 11, 2022
norm
Apr 11, 2022
Ali Çehreli
Apr 11, 2022
Salih Dincer
Apr 11, 2022
Paul Backus
Apr 11, 2022
Ali Çehreli
Apr 13, 2022
Salih Dincer
Apr 13, 2022
Ali Çehreli
Apr 13, 2022
H. S. Teoh
Apr 11, 2022
wjoe
Apr 11, 2022
Ali Çehreli
Apr 12, 2022
Dom DiSc
Apr 12, 2022
Ali Çehreli
Apr 12, 2022
wjoe
Apr 12, 2022
ag0aep6g
Apr 14, 2022
wjoe
Apr 14, 2022
ag0aep6g
Apr 17, 2022
wjoe
Apr 17, 2022
H. S. Teoh
Apr 17, 2022
ag0aep6g
Apr 17, 2022
ag0aep6g
Apr 17, 2022
H. S. Teoh
Apr 18, 2022
wjoe
Apr 18, 2022
H. S. Teoh
Apr 12, 2022
Ali Çehreli
Apr 14, 2022
wjoe
Apr 11, 2022
rikki cattermole
Apr 11, 2022
IGotD-
Apr 11, 2022
user1234
April 10, 2022

Hi All,

I am clearly misunderstanding something fundamental, and probably obvious :D

Reading some of the discussions on __metadata I was wondering if someone could explain why a immutable reference counting type is needed. By definition a reference counter cannot be immutable, so what would be the use case that requires it? It cannot really be pure nor safe either because the ref goes out of scope and the allocation is freed. How is this immutable?

Thanks,
Norm

April 11, 2022
immutable isn't tied to lifetime semantics.

It only says that this memory will never be modified by anyone during its lifetime.

Anyway, the real problem is with const. Both mutable and immutable become it automatically.
April 11, 2022
On Sunday, 10 April 2022 at 23:19:47 UTC, rikki cattermole wrote:
>
> immutable isn't tied to lifetime semantics.
>
> It only says that this memory will never be modified by anyone during its lifetime.

This is clearly where I am misunderstanding. In my mind immutable data means the data will not change and neither will the result of reading that data, ever.

I don't get how you can have thread safety guarantees based on immutable if reading that data in a thread suddenly becomes undefined behaviour and could return anything. That isn't immutable then. Once instantiated immutable data persists for the remainder of the program. You may not have access if the variable goes out of scope, but if you do it will always be there and always return the same value when you read from memory.

Thanks for replying, I am not trying to be argumentative here, just stating what I thought it meant and why I am confused. I'll be doing some more reading of the D spec to better understand immutability.

Cheers,
Norm


April 10, 2022
On 4/10/22 20:05, norm wrote:
> On Sunday, 10 April 2022 at 23:19:47 UTC, rikki cattermole wrote:

> In my mind immutable data
> means the data will not change and neither will the result of reading
> that data, ever.

Yes.

> I don't get how you can have thread safety guarantees based on immutable
> if reading that data in a thread suddenly becomes undefined behaviour
> and could return anything.

Yes, it would be a bug to attempt to read data that is not live anymore.

> That isn't immutable then.

The lifetime of immutable data can start and end.

import std.stdio;

struct S {
  int i;

  this(int i) {
    this.i = i;
    writeln(i, " is (about to be) alive!");
  }

  ~this() {
    writeln(i, " is (already) dead.");
  }
}

void main() {
  foreach (i; 0 .. 3) {
    immutable s = S(i);
  }
}

The output:

0 is (about to be) alive!
0 is (already) dead.
1 is (about to be) alive!
1 is (already) dead.
2 is (about to be) alive!
2 is (already) dead.

Module-level immutable and 'static const' would live much longer but they have a lifetime as well. However, today, the destructor cannot be executed on an immutable object, so I remove the qualifiers with cast(), which sohuld be undefined behavior (today?).

immutable(S) moduleS;

shared static this() {
  moduleS = S(42);
}

shared static ~this() {
  destroy(cast()moduleS);
}

void main() {
}

> Once instantiated
> immutable data persists for the remainder of the program.

That seems to be the misunderstanding. Again, I think module-level 'immutable' and 'static const' data fits that description.

> You may not
> have access if the variable goes out of scope, but if you do it will
> always be there and always return the same value when you read from memory.

That description fits D's GC-owned data (including immutables). The lifetime ends when there is no reference to it.

Another example is immutable messages passed between threads with std.concurrency: That kind of data clearly originates at run time and the receiving end keeps the data alive as long as it needs.

Ali

April 11, 2022
Storage classes like immutable/const/shared are not tied to any memory management strategy. Nor does it dictate memory lifetime.

It only dictates how it can be interacted with when you have a reference to it.
April 11, 2022
On Sunday, 10 April 2022 at 23:19:47 UTC, rikki cattermole wrote:
>
> immutable isn't tied to lifetime semantics.
>
> It only says that this memory will never be modified by anyone during its lifetime.
>
> Anyway, the real problem is with const. Both mutable and immutable become it automatically.

I was thinking about that, often when using const you use it when passing parameters to functions. This is essentially borrowing. The situation is similar with C++ with unique_ptr and shared_ptr. Often C++ interfaces use const* when using pointers and not their smart pointer counterparts, so essentially the ownership remains, while "borrowing" are using raw pointers. A C++ interface only accepts the smart pointers when you want to change ownership. D could use a similar approach, when using pointers/references you shouldn't alter the internal data including reference count.

What I would interested in is if D could have move by default depending on type. In this case the RC pointer wrapper could be move by default. Increasing is only done when calling "clone" (similar to Rust). This way RC increases are optimized naturally. What I don't want from Rust is the runtime aliasing check (RefCell) on at all times. I rather go with that the compiler assumes no aliasing but the programmer is responsible for this. You can have runtime aliasing/borrowing check in debug mode but in release build it can be removed. This is similar to bounds checking where you can choose to have it or not.
April 11, 2022

On Sunday, 10 April 2022 at 23:05:24 UTC, norm wrote:

>

Hi All,

I am clearly misunderstanding something fundamental, and probably obvious :D

Reading some of the discussions on __metadata I was wondering if someone could explain why a immutable reference counting type is needed. By definition a reference counter cannot be immutable, so what would be the use case that requires it? It cannot really be pure nor safe either because the ref goes out of scope and the allocation is freed. How is this immutable?

Thanks,
Norm

refcounting would require a concept of "tail const" / "tail immutable" so that transitivity of the qualifier does not affect the data used to refcount (basically the field that hold the count) but only the data that are refcounted.

April 11, 2022

On Monday, 11 April 2022 at 03:24:11 UTC, Ali Çehreli wrote:

>

The output:

0 is (about to be) alive!
0 is (already) dead.
1 is (about to be) alive!
1 is (already) dead.
2 is (about to be) alive!
2 is (already) dead.

It worked for me in a different way.

>

1 is (about to be) alive!
2 is (already) dead.
2 is (already) dead.
2 is (already) dead.

>

2 is (already) dead.
2 is (already) dead.
Hello D!
1 is (already) dead.

Because I changed the code like this.


struct S
{
  . . .

  string toString() { return ""; }
}

//S test(inout S s)/*
S test(S s)//*/
{
  s.i = 2;
  return s;
}

void main()
{
  immutable s = S(1);
  test(s).writeln;
  "Hello".writefln!"%s D!";
}

If the inout is set, it does not allow compilation.

Thanks, SDB79

April 11, 2022
On Monday, 11 April 2022 at 03:24:11 UTC, Ali Çehreli wrote:
> On 4/10/22 20:05, norm wrote:
> > On Sunday, 10 April 2022 at 23:19:47 UTC, rikki cattermole
> wrote:
>
> > In my mind immutable data
> > means the data will not change and neither will the result of
> reading
> > that data, ever.
>
> Yes.
>
> > I don't get how you can have thread safety guarantees based
> on immutable
> > if reading that data in a thread suddenly becomes undefined
> behaviour
> > and could return anything.
>
> Yes, it would be a bug to attempt to read data that is not live anymore.
>
> > That isn't immutable then.
>
> The lifetime of immutable data can start and end.
>
> import std.stdio;
>
> struct S {
>   int i;
>
>   this(int i) {
>     this.i = i;
>     writeln(i, " is (about to be) alive!");
>   }
>
>   ~this() {
>     writeln(i, " is (already) dead.");
>   }
> }
>
> void main() {
>   foreach (i; 0 .. 3) {
>     immutable s = S(i);
>   }
> }
>
> The output:
>
> 0 is (about to be) alive!
> 0 is (already) dead.
> 1 is (about to be) alive!
> 1 is (already) dead.
> 2 is (about to be) alive!
> 2 is (already) dead.
>
> Module-level immutable and 'static const' would live much longer but they have a lifetime as well. However, today, the destructor cannot be executed on an immutable object, so I remove the qualifiers with cast(), which sohuld be undefined behavior (today?).
>
> immutable(S) moduleS;
>
> shared static this() {
>   moduleS = S(42);
> }
>
> shared static ~this() {
>   destroy(cast()moduleS);
> }
>
> void main() {
> }
>
> > Once instantiated
> > immutable data persists for the remainder of the program.
>
> That seems to be the misunderstanding. Again, I think module-level 'immutable' and 'static const' data fits that description.
>
> > You may not
> > have access if the variable goes out of scope, but if you do
> it will
> > always be there and always return the same value when you
> read from memory.
>
> That description fits D's GC-owned data (including immutables). The lifetime ends when there is no reference to it.
>
> Another example is immutable messages passed between threads with std.concurrency: That kind of data clearly originates at run time and the receiving end keeps the data alive as long as it needs.
>
> Ali

To my understanding immutable data should reside in a separate data segment which itself could reside in ROM.
So when the variable, or 'pointer' to the data, goes out of scope just the 'pointer' is gone, the actual data unreachable, but still there.
Due to its immutable nature immutable data can't be changed and this, to my understanding, includes deallocation. And because the data could be in ROM any modification is an error. How would you deallocate ROM anyways?
Your foreach could be unrolled at compile time, however it could easily be changed to a runtime only loop but this entire concept of creating immutable data on the fly doesn't make sense to me - that should be const's domain. Strings, I know. But the way things are, I hardly see a difference between immutable and const.
April 11, 2022

On Monday, 11 April 2022 at 12:12:39 UTC, Salih Dincer wrote:

>

It worked for me in a different way.

>

1 is (about to be) alive!
2 is (already) dead.
2 is (already) dead.
2 is (already) dead.

>

2 is (already) dead.
2 is (already) dead.
Hello D!
1 is (already) dead.

Because I changed the code like this.


struct S
{
  . . .

  string toString() { return ""; }
}

//S test(inout S s)/*
S test(S s)//*/
{
  s.i = 2;
  return s;
}

void main()
{
  immutable s = S(1);
  test(s).writeln;
  "Hello".writefln!"%s D!";
}

If the inout is set, it does not allow compilation.

Because S does not contain any pointers or references, you are allowed to create a mutable copy of an S from an immutable source. That's what happens when you pass it to test.

The extra destructor calls come from the copies made by test and writeln. If you add a copy constructor, you can see this happening in the output:

struct S {

    /* ... */

    this(ref inout typeof(this) other) inout {
        this.i = other.i;
        writeln(i, " was copied.");
    }
}
1 is (about to be) alive!
1 was copied.
2 was copied.
2 is (already) dead.
2 was copied.
2 was copied.
2 was copied.
2 is (already) dead.
2 is (already) dead.

2 is (already) dead.
2 is (already) dead.
Hello D!
1 is (already) dead.

1 constructor call + 5 copies = 6 destructor calls.

« First   ‹ Prev
1 2 3