Jump to page: 1 2
Thread overview
What functions can be called on a shared struct that's implicitly castable to immutable?
Nov 03, 2013
Simen Kjærås
Nov 04, 2013
deadalnix
Nov 04, 2013
Simen Kjærås
Nov 04, 2013
deadalnix
Nov 04, 2013
Simen Kjærås
Nov 04, 2013
deadalnix
Nov 05, 2013
Simen Kjærås
Nov 05, 2013
deadalnix
Nov 06, 2013
Simen Kjærås
Nov 06, 2013
deadalnix
Nov 06, 2013
Simen Kjærås
Nov 06, 2013
deadalnix
Nov 06, 2013
Simen Kjærås
Nov 04, 2013
Maxim Fomin
Nov 04, 2013
Simen Kjærås
November 03, 2013
Consider:

module foo;

struct S {
    immutable(int)[] arr;
    void fuzz() const pure {
    }
}

void bar(S s) {
    s.fuzz();
}

void main() {
    shared S s;
    bar(s);   // a
    s.fuzz(); // b
}


In this case, the line marked 'a' works perfectly - it compiles and does what I'd expect it to.

However,the line marked 'b' does not compile - " non-shared const method foo.S.fuzz is not callable using a shared mutable object ".

The reason for this is that fuzz takes the 'this' pointer by reference, and so risks that another thread corrupt the internal state while it's working.

Seeing as line 'a' works, it seems safe for line 'b' to make a copy behind the scenes (structs in D are defined to have cheap copying, I seem to recall) and call fuzz on that copy - the type system claims that the call to fuzz cannot possibly change the state of the struct on which it is called.

With the state of 'shared' as it is, I'm unsure if this is a change that should be done - it seems perhaps better to wait for a true overhaul of the feature.

Where I specifically found a need for this is in attempting to fix bug 11188[1]. While I have found a workaround, it's needed per function, and I'm pretty sure it'll break some specific other cases.

[1]: http://d.puremagic.com/issues/show_bug.cgi?id=11188

--
  Simen
November 04, 2013
On Sunday, 3 November 2013 at 21:55:22 UTC, Simen Kjærås wrote:
> Consider:
>
> module foo;
>
> struct S {
>     immutable(int)[] arr;
>     void fuzz() const pure {
>     }
> }
>
> void bar(S s) {
>     s.fuzz();
> }
>
> void main() {
>     shared S s;
>     bar(s);   // a
>     s.fuzz(); // b
> }
>
>
> In this case, the line marked 'a' works perfectly - it compiles and does what I'd expect it to.
>
> However,the line marked 'b' does not compile - " non-shared const method foo.S.fuzz is not callable using a shared mutable object ".
>

It is because a imply a pass b value, when b a pass by reference.
November 04, 2013
On 04.11.2013 06:53, deadalnix wrote:
> On Sunday, 3 November 2013 at 21:55:22 UTC, Simen Kjærås wrote:
>> Consider:
>>
>> module foo;
>>
>> struct S {
>>     immutable(int)[] arr;
>>     void fuzz() const pure {
>>     }
>> }
>>
>> void bar(S s) {
>>     s.fuzz();
>> }
>>
>> void main() {
>>     shared S s;
>>     bar(s);   // a
>>     s.fuzz(); // b
>> }
>>
>>
>> In this case, the line marked 'a' works perfectly - it compiles and does what I'd expect it to.
>>
>> However,the line marked 'b' does not compile - " non-shared const method foo.S.fuzz is not callable using a shared mutable object ".
>>
>
> It is because a imply a pass b value, when b a pass by reference.
Indeed. That's why I wrote that one line further down. That's not the point. The point is I have to jump through no hoops to get line a to compile - no 'assumeUnshared', no cast, no explicit copying.

On that basis, I argue that line b could be made to work by silently creating a copy of s, because the call to fuzz is guaranteed not to change s. There may be problems with this that I have not considered. If you know of any, please do tell.

I guess it might be conceivable that fuzz waits for a synchronization message (but is that really possible in a pure const function?), which will never happen for the copy, but is this even a case we want to support?

--
  Simen
November 04, 2013
On Monday, 4 November 2013 at 08:14:57 UTC, Simen Kjærås wrote:
>
> On 04.11.2013 06:53, deadalnix wrote:
>> On Sunday, 3 November 2013 at 21:55:22 UTC, Simen Kjærås wrote:
>>> Consider:
>>>
>>> module foo;
>>>
>>> struct S {
>>>    immutable(int)[] arr;
>>>    void fuzz() const pure {
>>>    }
>>> }
>>>
>>> void bar(S s) {
>>>    s.fuzz();
>>> }
>>>
>>> void main() {
>>>    shared S s;
>>>    bar(s);   // a
>>>    s.fuzz(); // b
>>> }
>>>
>>>
>>> In this case, the line marked 'a' works perfectly - it compiles and does what I'd expect it to.
>>>
>>> However,the line marked 'b' does not compile - " non-shared const method foo.S.fuzz is not callable using a shared mutable object ".
>>>
>>
>> It is because a imply a pass b value, when b a pass by reference.
> Indeed. That's why I wrote that one line further down. That's not the point. The point is I have to jump through no hoops to get line a to compile - no 'assumeUnshared', no cast, no explicit copying.
>
> On that basis, I argue that line b could be made to work by silently creating a copy of s, because the call to fuzz is guaranteed not to change s. There may be problems with this that I have not considered. If you know of any, please do tell.
>
> I guess it might be conceivable that fuzz waits for a synchronization message (but is that really possible in a pure const function?), which will never happen for the copy, but is this even a case we want to support?
>
> --
>   Simen

You are trying to solve a non problem here. s is not shared in the first place, so you can start by fixing it here.

Now, if S has some indirection, then it make sense to mark it as shared, but then the trick you propose won't work.
November 04, 2013
On 04.11.2013 09:46, deadalnix wrote:
> You are trying to solve a non problem here. s is not shared in the first
> place, so you can start by fixing it here.
>
> Now, if S has some indirection, then it make sense to mark it as shared,
> but then the trick you propose won't work.

I believe we're talking past each other here. Perhaps a better example:

module foo;

struct S {
   immutable(int)[] arr;
   void fuzz() const pure {
   }
}

void bar(S s) {
   s.fuzz();
}

void main() {
   shared S* s = new shared S;
   bar(*s);   // a
   s.fuzz(); // b
}


This example demonstrates the exact same behavior as before, and again I'm not doing a lot of anything special to get line a to compile.

Now, I'm still of the opinion that the 'trick' I propose works - making a copy of s is cheap (as that's what the spec says about copying structs), s is implicitly castable to immutable, and the function to be called is guaranteed not to change s in any way. From all that, it seems safe to call fuzz on a shared S.
November 04, 2013
On Sunday, 3 November 2013 at 21:55:22 UTC, Simen Kjærås wrote:
> Consider:
>
> module foo;
>
> struct S {
>     immutable(int)[] arr;
>     void fuzz() const pure {
>     }
> }
>
> void bar(S s) {
>     s.fuzz();
> }
>
> void main() {
>     shared S s;
>     bar(s);   // a
>     s.fuzz(); // b
> }
>
>
> In this case, the line marked 'a' works perfectly - it compiles and does what I'd expect it to.
>
> However,the line marked 'b' does not compile - " non-shared const method foo.S.fuzz is not callable using a shared mutable object ".
>
> The reason for this is that fuzz takes the 'this' pointer by reference, and so risks that another thread corrupt the internal state while it's working.
>
> Seeing as line 'a' works, it seems safe for line 'b' to make a copy behind the scenes

Why should method call invole copy? This breaks lots of assumptions and it would be a pretty weird corner case.

> (structs in D are defined to have cheap copying, I seem to recall)

Why? There is no restrictions on postblit, so copying may be cheap as well as arbitrary costly.

> and call fuzz on that copy - the type system claims that the call to fuzz cannot possibly change the state of the struct on which it is called.

Your problem may be solved pretty simple:

pure fuzz(S s)
{
	
}

and be happy with s.fuzz() operating and copy if you want so. Does it address your problem?

> With the state of 'shared' as it is, I'm unsure if this is a change that should be done - it seems perhaps better to wait for a true overhaul of the feature.

What shared means in D is not trivial question, I agree.
November 04, 2013
On 04.11.2013 11:28, Maxim Fomin wrote:
> Why should method call invole copy? This breaks lots of assumptions and
> it would be a pretty weird corner case.

The idea was that it's basically an optimization. The type system guarantees that there is no difference.


>> (structs in D are defined to have cheap copying, I seem to recall)
>
> Why? There is no restrictions on postblit, so copying may be cheap as
> well as arbitrary costly.

I do not have a reference handy, but Andrei or Walter has stated in the past that it is safe to assume that copying is cheap.


>> and call fuzz on that copy - the type system claims that the call to
>> fuzz cannot possibly change the state of the struct on which it is
>> called.
>
> Your problem may be solved pretty simple:
>
> pure fuzz(S s)
> {
>
> }
>
> and be happy with s.fuzz() operating and copy if you want so. Does it
> address your problem?

Not really. My specific problem is with std.math.abs and shared BigInt. Bug 11188 explains how it is not possible to call abs((shared BigInt).init). The reason for that is that the operator overloads are not marked shared. They are however both const and pure. As we know, operator overloads may not be defined outside the aggregate type.

--
  Simen
November 04, 2013
On Monday, 4 November 2013 at 09:34:52 UTC, Simen Kjærås wrote:
> On 04.11.2013 09:46, deadalnix wrote:
>> You are trying to solve a non problem here. s is not shared in the first
>> place, so you can start by fixing it here.
>>
>> Now, if S has some indirection, then it make sense to mark it as shared,
>> but then the trick you propose won't work.
>
> I believe we're talking past each other here. Perhaps a better example:
>
> module foo;
>
> struct S {
>    immutable(int)[] arr;
>    void fuzz() const pure {
>    }
> }
>
> void bar(S s) {
>    s.fuzz();
> }
>
> void main() {
>    shared S* s = new shared S;
>    bar(*s);   // a
>    s.fuzz(); // b
> }
>
>
> This example demonstrates the exact same behavior as before, and again I'm not doing a lot of anything special to get line a to compile.
>
> Now, I'm still of the opinion that the 'trick' I propose works - making a copy of s is cheap (as that's what the spec says about copying structs), s is implicitly castable to immutable, and the function to be called is guaranteed not to change s in any way. From all that, it seems safe to call fuzz on a shared S.

Any read of arr in fuzz won't be sequentially consistent, so it would be incorrect to let you call fuzz on a shared object.
November 05, 2013
On 04.11.2013 20:52, deadalnix wrote:
> On Monday, 4 November 2013 at 09:34:52 UTC, Simen Kjærås wrote:
>> On 04.11.2013 09:46, deadalnix wrote:
>>> You are trying to solve a non problem here. s is not shared in the first
>>> place, so you can start by fixing it here.
>>>
>>> Now, if S has some indirection, then it make sense to mark it as shared,
>>> but then the trick you propose won't work.
>>
>> I believe we're talking past each other here. Perhaps a better example:
>>
>> module foo;
>>
>> struct S {
>>    immutable(int)[] arr;
>>    void fuzz() const pure {
>>    }
>> }
>>
>> void bar(S s) {
>>    s.fuzz();
>> }
>>
>> void main() {
>>    shared S* s = new shared S;
>>    bar(*s);   // a
>>    s.fuzz(); // b
>> }
>>
>>
>> This example demonstrates the exact same behavior as before, and again
>> I'm not doing a lot of anything special to get line a to compile.
>>
>> Now, I'm still of the opinion that the 'trick' I propose works -
>> making a copy of s is cheap (as that's what the spec says about
>> copying structs), s is implicitly castable to immutable, and the
>> function to be called is guaranteed not to change s in any way. From
>> all that, it seems safe to call fuzz on a shared S.
>
> Any read of arr in fuzz won't be sequentially consistent, so it would be
> incorrect to let you call fuzz on a shared object.

Thank you, that's what I wanted to hear. I don't agree though.

Sequential consistence means reads and writes on one processor happen in the order they're written, possibly interleaved with reads and writes from other processors. I cannot see how this promise is broken by taking a copy on which you only do reads.

Example (CPU 1 is running fuzz, CPU 2 is randomly mutating memory, or doing something worthwhile):

  | CPU 1        | CPU 2
==============================
1 | read(arr)    | write(arr)
2 | read(arr[0]) | read(arr)
3 | read(arr[1]) | write(arr[0])

Now, one sequentially consistent ordering of these operations would be:

  | CPU 1        | CPU 2
==============================
1 | read(arr)    |
2 | read(arr[0]) |
3 | read(arr[1]) |
4 |              | write(arr)
5 |              | read(arr)
6 |              | write(arr[0])

Which is exactly what happens in the case of taking a copy.

An equally valid ordering would of course be this:

  | CPU 1        | CPU 2
==============================
1 | read(arr)    |
2 |              | write(arr)
3 |              | read(arr)
4 |              | write(arr[0])
5 | read(arr[0]) |
6 | read(arr[1]) |

In which case the results would potentially be different. However, the fact that other sequentially consistent orderings exist does not mean that the first is wrong.


Can you please show an example of how copying does violate sequential consistency?

--
  Simen
November 05, 2013
On Tuesday, 5 November 2013 at 17:37:26 UTC, Simen Kjærås wrote:
> Thank you, that's what I wanted to hear. I don't agree though.
>
> Sequential consistence means reads and writes on one processor happen in the order they're written, possibly interleaved with reads and writes from other processors. I cannot see how this promise is broken by taking a copy on which you only do reads.
>

No, that mean that read and write for ALL processor are done in order.

> Example (CPU 1 is running fuzz, CPU 2 is randomly mutating memory, or doing something worthwhile):
>
>   | CPU 1        | CPU 2
> ==============================
> 1 | read(arr)    | write(arr)
> 2 | read(arr[0]) | read(arr)
> 3 | read(arr[1]) | write(arr[0])
>
> Now, one sequentially consistent ordering of these operations would be:
>
>   | CPU 1        | CPU 2
> ==============================
> 1 | read(arr)    |
> 2 | read(arr[0]) |
> 3 | read(arr[1]) |
> 4 |              | write(arr)
> 5 |              | read(arr)
> 6 |              | write(arr[0])
>
> Which is exactly what happens in the case of taking a copy.
>
> An equally valid ordering would of course be this:
>
>   | CPU 1        | CPU 2
> ==============================
> 1 | read(arr)    |
> 2 |              | write(arr)
> 3 |              | read(arr)
> 4 |              | write(arr[0])
> 5 | read(arr[0]) |
> 6 | read(arr[1]) |
>
> In which case the results would potentially be different. However, the fact that other sequentially consistent orderings exist does not mean that the first is wrong.
>

Without sequential consistency, CPU1 can see the operation of CPU2 in the wrong order, as it won't ensure it is not working with outdated values.
« First   ‹ Prev
1 2