www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Compiler could elide many more postblit constructor calls

reply "TommiT" <tommitissari hotmail.com> writes:
Disclaimer: The "discovery" I'm about to describe here seems so 
obvious that I'm inclined to think that I've made some mistake.

The sole purpose of postblit constructors is to provide value 
semantics to structs which have mutable indirection (variables 
which have only immutable indirection have value semantics 
implicitly). Variables that are const/immutable can't have 
mutable indirection. Therefore, when making a copy from a 
const/immutable variable to a const/immutable variable, there's 
no need to call the postblit constructor.

Example:

----
struct S
{
     int[] values;

     this(this)
     {
         values = values.dup;
     }
}

void foo(const S) { }

void main()
{
     const S s;
     foo(s); // No need to call postblit
}
Jun 29 2013
next sibling parent reply "TommiT" <tommitissari hotmail.com> writes:
On Saturday, 29 June 2013 at 13:47:36 UTC, TommiT wrote:
 [..]

 Example:

 ----
 struct S
 {
     int[] values;

     this(this)
     {
         values = values.dup;
     }
 }

 void foo(const S) { }

 void main()
 {
     const S s;
     foo(s); // No need to call postblit
 }
One important related detail: If the compiler decides to elide the postblit of S on the call to foo(s), then the destructor of S (if S happened to have one) should not be called when the call to foo exits and the argument passed to foo goes out of scope. The logic behind this is that when we omit the postblit on the argument that's passed by value, it is as-if we had passed the argument by const reference (except that the argument is considered local to foo, i.e. not returnable by reference and what not).
Jun 29 2013
parent reply "Diggory" <diggsey googlemail.com> writes:
On Saturday, 29 June 2013 at 17:57:33 UTC, TommiT wrote:
 On Saturday, 29 June 2013 at 13:47:36 UTC, TommiT wrote:
 [..]

 Example:

 ----
 struct S
 {
    int[] values;

    this(this)
    {
        values = values.dup;
    }
 }

 void foo(const S) { }

 void main()
 {
    const S s;
    foo(s); // No need to call postblit
 }
One important related detail: If the compiler decides to elide the postblit of S on the call to foo(s), then the destructor of S (if S happened to have one) should not be called when the call to foo exits and the argument passed to foo goes out of scope. The logic behind this is that when we omit the postblit on the argument that's passed by value, it is as-if we had passed the argument by const reference (except that the argument is considered local to foo, i.e. not returnable by reference and what not).
Unless the function is pure, this is only possible for immutable parameters otherwise the original variable may be modified while inside the function even if it is passed by const.
Jun 29 2013
next sibling parent reply "TommiT" <tommitissari hotmail.com> writes:
On Sunday, 30 June 2013 at 02:20:24 UTC, Diggory wrote:
 On Saturday, 29 June 2013 at 17:57:33 UTC, TommiT wrote:
 On Saturday, 29 June 2013 at 13:47:36 UTC, TommiT wrote:
 [..]

 Example:

 ----
 struct S
 {
   int[] values;

   this(this)
   {
       values = values.dup;
   }
 }

 void foo(const S) { }

 void main()
 {
   const S s;
   foo(s); // No need to call postblit
 }
One important related detail: If the compiler decides to elide the postblit of S on the call to foo(s), then the destructor of S (if S happened to have one) should not be called when the call to foo exits and the argument passed to foo goes out of scope. The logic behind this is that when we omit the postblit on the argument that's passed by value, it is as-if we had passed the argument by const reference (except that the argument is considered local to foo, i.e. not returnable by reference and what not).
Unless the function is pure, this is only possible for immutable parameters otherwise the original variable may be modified while inside the function even if it is passed by const.
I'm not sure I follow. Are you saying... 1) the function foo could cast away const and modify s or 2) some other thread could modify s while foo is executing case 1: void foo(const S s) { S m = cast(S) s; s.values[0] = 42; } I'm not sure how this is in D, but I think in C++, casting away const and then modifying the variable is potentially undefined behaviour. I think the compiler should be free to assume that the programmer hasn't written code that has undefined behaviour. case 2: I don't think another thread could be modifying s, because it's not shared, i.e. it's a thread local variable. If the parameter to foo was "shared const S", then it could be modified by another thread, and thus the postblit could not be elided.
Jun 30 2013
parent reply "anonymous" <anonymous example.com> writes:
On Sunday, 30 June 2013 at 07:27:06 UTC, TommiT wrote:
 On Sunday, 30 June 2013 at 02:20:24 UTC, Diggory wrote:
 On Saturday, 29 June 2013 at 17:57:33 UTC, TommiT wrote:
 On Saturday, 29 June 2013 at 13:47:36 UTC, TommiT wrote:
 [..]

 Example:

 ----
 struct S
 {
  int[] values;

  this(this)
  {
      values = values.dup;
  }
 }

 void foo(const S) { }

 void main()
 {
  const S s;
  foo(s); // No need to call postblit
 }
[...]
 Unless the function is pure, this is only possible for 
 immutable parameters otherwise the original variable may be 
 modified while inside the function even if it is passed by 
 const.
I'm not sure I follow. Are you saying... 1) the function foo could cast away const and modify s or 2) some other thread could modify s while foo is executing
3) foo mutates the data through a mutable global. int[] data = [1, 2, 3]; void foo(const S) { data[0] = 42; } void main() { const S s = S(data); foo(s); } Pure functions can't access globals, so they're fine here. (I'm rather ignorant about that whole postblit thing, just trying to clarify Diggory's point as I understand it.)
Jun 30 2013
parent reply "TommiT" <tommitissari hotmail.com> writes:
On Sunday, 30 June 2013 at 08:16:44 UTC, anonymous wrote:
 On Sunday, 30 June 2013 at 07:27:06 UTC, TommiT wrote:
 On Sunday, 30 June 2013 at 02:20:24 UTC, Diggory wrote:
 On Saturday, 29 June 2013 at 17:57:33 UTC, TommiT wrote:
 On Saturday, 29 June 2013 at 13:47:36 UTC, TommiT wrote:
 [..]

 Example:

 ----
 struct S
 {
 int[] values;

 this(this)
 {
     values = values.dup;
 }
 }

 void foo(const S) { }

 void main()
 {
 const S s;
 foo(s); // No need to call postblit
 }
[...]
 Unless the function is pure, this is only possible for 
 immutable parameters otherwise the original variable may be 
 modified while inside the function even if it is passed by 
 const.
I'm not sure I follow. Are you saying... 1) the function foo could cast away const and modify s or 2) some other thread could modify s while foo is executing
3) foo mutates the data through a mutable global. int[] data = [1, 2, 3]; void foo(const S) { data[0] = 42; } void main() { const S s = S(data); foo(s); } Pure functions can't access globals, so they're fine here. (I'm rather ignorant about that whole postblit thing, just trying to clarify Diggory's point as I understand it.)
If this is indeed Diggory's point, it's not very pointy (sorry). The compiler should be able to safely omit the postblit on 's' when it passes it to foo (and the possible destructor of foo's argument inside foo when foo exits).
Jun 30 2013
next sibling parent reply "anonymous" <anonymous example.com> writes:
On Sunday, 30 June 2013 at 08:34:20 UTC, TommiT wrote:
 On Sunday, 30 June 2013 at 08:16:44 UTC, anonymous wrote:
 On Sunday, 30 June 2013 at 07:27:06 UTC, TommiT wrote:
 On Sunday, 30 June 2013 at 02:20:24 UTC, Diggory wrote:
 On Saturday, 29 June 2013 at 17:57:33 UTC, TommiT wrote:
 On Saturday, 29 June 2013 at 13:47:36 UTC, TommiT wrote:
 [..]

 Example:

 ----
 struct S
 {
 int[] values;

 this(this)
 {
    values = values.dup;
 }
 }

 void foo(const S) { }

 void main()
 {
 const S s;
 foo(s); // No need to call postblit
 }
[...]
 Unless the function is pure, this is only possible for 
 immutable parameters otherwise the original variable may be 
 modified while inside the function even if it is passed by 
 const.
I'm not sure I follow. Are you saying... 1) the function foo could cast away const and modify s or 2) some other thread could modify s while foo is executing
3) foo mutates the data through a mutable global. int[] data = [1, 2, 3]; void foo(const S) { data[0] = 42; } void main() { const S s = S(data); foo(s); } Pure functions can't access globals, so they're fine here. (I'm rather ignorant about that whole postblit thing, just trying to clarify Diggory's point as I understand it.)
If this is indeed Diggory's point, it's not very pointy (sorry). The compiler should be able to safely omit the postblit on 's' when it passes it to foo (and the possible destructor of foo's argument inside foo when foo exits).
void foo(const S s) { data[0] = 42; assert(s.values[0] == 1); } With the postblit, the assert holds. Without it, it would fail. Isn't that problematic?
Jun 30 2013
parent "TommiT" <tommitissari hotmail.com> writes:
On Sunday, 30 June 2013 at 08:59:14 UTC, anonymous wrote:
 void foo(const S s)
 {
     data[0] = 42;
     assert(s.values[0] == 1);
 }

 With the postblit, the assert holds. Without it, it would fail. 
 Isn't that problematic?
Ok, I see your point now. The function needs to be pure to elide postblit on a copy from a const variable to a const variable. The function doesn't need to be pure to elide postblit on a copy from immutable variable to const/immutable variable. Although, if the compiler sees the function body, it could probably do some clever static analysis to see if it's possible that the const data is mutated, and elide the postblit if the answer is "no".
Jun 30 2013
prev sibling parent reply "Diggory" <diggsey googlemail.com> writes:
 If this is indeed Diggory's point, it's not very pointy 
 (sorry). The compiler should be able to safely omit the 
 postblit on 's' when it passes it to foo (and the possible 
 destructor of foo's argument inside foo when foo exits).
If the postblit is omitted, and the function or code it calls accesses the const variable through a global mutable reference then that function will be able to see those changes despite the parameter being passed by value. Without this optimisation it would not be able to see the changes. Also, the function needs to be strongly pure, not just weakly pure, otherwise the calling code could pass in both the const variable and a non-const reference to that variable. If the callee modifies the non-const reference it would see the changes to the const parameter which it shouldn't be able to do.
Jun 30 2013
parent reply "TommiT" <tommitissari hotmail.com> writes:
On Sunday, 30 June 2013 at 09:24:28 UTC, Diggory wrote:
 [..]

 Also, the function needs to be strongly pure, not just weakly 
 pure, otherwise the calling code could pass in both the const 
 variable and a non-const reference to that variable. If the 
 callee modifies the non-const reference it would see the 
 changes to the const parameter which it shouldn't be able to do.
Good point. So, in order to elide postblit when a const variable is passed by value as a const argument, the function would not only need to be pure, but also all of its parameters would have to be either const or immutable.
Jun 30 2013
parent reply "TommiT" <tommitissari hotmail.com> writes:
On Sunday, 30 June 2013 at 09:38:49 UTC, TommiT wrote:
 [..] So, in order to elide postblit when a const variable is 
 passed by value as a const argument, the function would not 
 only need to be pure, but also all of its parameters would have 
 to be either const or immutable.
All along I've been saying "when const variable is passed to a function by const value...", but the postblit elision can be done also when a mutable variable is passed by const value (to a strongly pure function).
Jun 30 2013
parent reply "TommiT" <tommitissari hotmail.com> writes:
To take the full advantage of this optimization, a change in 
lambda function parameter type inference might be necessary, 
which sadly would be a breaking change:

struct S
{
     int get()
     {
         return 11;
     }

     int get() const
     {
         return 22;
     }
}

void main()
{
     S s;
     int n = (a) { return a.get(); }(s); // [1]
     writeln(n); // [2]
}

[1]: I think this lambda instantiates to something like:
int __fun(S a) { return a.get(); }
...but it'd be better if it first attempted to instantiate to:
int __fun(const S a) { return a.get(); }
...and resort to passing by mutable value only if the first 
attempt fails.

[2]: Currently prints 11, but would print 22 if this breaking 
change were made.
Jun 30 2013
parent "TommiT" <tommitissari hotmail.com> writes:
Yet one small observation: This optimization would mean that a 
lot of the use cases of "auto ref const MyType" parameters (the 
upcoming non-templated auto ref feature... although I don't know 
if that's the syntax for it) could be replaced by using "const 
MyType" parameters.

Or, if you look at it from the other side of the coin: if you 
always took function arguments by "auto ref const MyType", there 
wouldn't be any functions to apply this optimization for.
Jul 01 2013
prev sibling parent "TommiT" <tommitissari hotmail.com> writes:
On Sunday, 30 June 2013 at 07:27:06 UTC, TommiT wrote:
 [..]

 case 1:
 void foo(const S s)
 {
     S m = cast(S) s;
     s.values[0] = 42;
 }
A typo. It should be: case 1: void foo(const S s) { S m = cast(S) s; m.values[0] = 42; } On Sunday, 30 June 2013 at 02:20:24 UTC, Diggory wrote:
 Unless the function is pure, this is only possible for [..]
I don't see what kind of a difference the pureness of foo would make in either of those two cases I wrote about in my previous post.
Jun 30 2013
prev sibling next sibling parent reply "TommiT" <tommitissari hotmail.com> writes:
Perhaps an interesting observation about this is that while in 
C++ const correctness literally never improves performance [1], 
in D (if these optimizations were implemented) const correctness 
could potentially increase performance considerably (if there are 
expensive postblits). Thus, functions should take arguments by 
const value rather than value, when the type of the parameter is 
either templated or has postblit.

[1] Except with constant folding or when the compiler can put 
const data to rommable memory and then share it with multiple 
instances of the same program.
Jun 30 2013
parent "Diggory" <diggsey googlemail.com> writes:
On Sunday, 30 June 2013 at 10:21:12 UTC, TommiT wrote:
 Perhaps an interesting observation about this is that while in 
 C++ const correctness literally never improves performance [1], 
 in D (if these optimizations were implemented) const 
 correctness could potentially increase performance considerably 
 (if there are expensive postblits). Thus, functions should take 
 arguments by const value rather than value, when the type of 
 the parameter is either templated or has postblit.

 [1] Except with constant folding or when the compiler can put 
 const data to rommable memory and then share it with multiple 
 instances of the same program.
Precisely, although something that could improve performance even more would be escape analysis. In this example, if the compiler can prove that there are no global/parameter mutable references to a given variable then it will be able to apply the optimisation regardless of purity.
Jun 30 2013
prev sibling parent "TommiT" <tommitissari hotmail.com> writes:
I posted an enhancement request for this:

http://d.puremagic.com/issues/show_bug.cgi?id=10527
Jul 03 2013