www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - opIndexMember

reply claptrap <clap trap.com> writes:
In some cases a structure of arrays is more desirable than an 
array of structs, usually for performance reasons, but maybe you 
want tight packing or something else, so I have been thinking a 
while about proposing the following...

auto opIndexMember!(string what)(size_t idx);

So basically if opIndexMember is defined for Foo then...

foo[i].x

Is rewritten to..

foo.opIndexMember!("x")(i);

So you can do stuff like this....

struct Vector { float x,y,z; }

struct VecArray
{
      float[] _x;
      float[] _y;
      float[] _z;

     auto opIndexMember(string what)(size_t idx)
     {
         static if (what = "x") return _x[idx];
         else static if (what = "y") return _y[idx];
         else static if (what = "z") return _z[idx];
         else static if (what = "magitude") return
             sqrt(sqr(_x[idx])+sqr(_y[idx])+sqr(_z[idx]));
         else static if (what = "") return Vector(_x{idx], 
_y[idx], _z[idx]);

         static assert(0);
     }
}

So you can use that either as a structure of arrays, or an array 
of structs. And with introspection and UDAs you could probably 
have an array class that automatically enumerates members of the 
struct, stores as SOA internally, but provides a AOS API.

Im thinking maybe disallow having both opIndex and opIndexMember 
defined, if you use array op but with no trailing member it just 
passes an empty string. Might reduce ambiguity?
Dec 27 2020
next sibling parent reply Dennis <dkorpel gmail.com> writes:
On Sunday, 27 December 2020 at 14:46:43 UTC, claptrap wrote:
 So you can do stuff like this....
What's wrong with this? ``` struct VecArray { float[] _x; float[] _y; float[] _z; Vector opIndex(size_t idx) { return Vector(_x[idx], _y[idx], _z[idx]); } } ``` Performance in debug builds?
Dec 27 2020
parent reply claptrap <clap trap.com> writes:
On Sunday, 27 December 2020 at 15:02:16 UTC, Dennis wrote:
 On Sunday, 27 December 2020 at 14:46:43 UTC, claptrap wrote:
 So you can do stuff like this....
What's wrong with this? ``` struct VecArray { float[] _x; float[] _y; float[] _z; Vector opIndex(size_t idx) { return Vector(_x[idx], _y[idx], _z[idx]); } } ``` Performance in debug builds?
It wont work with ref returns, and performance in release builds. I had some instances where the return type was being fully constructed even though only one member was accessed, but I dont remember exactly what code caused that. godbolt with LDC -03... =============================================== import std; struct Vector { float x,y,z; } struct VecArray { float[] _x; float[] _y; float[] _z; Vector opIndex(size_t idx) { return Vector(_x[idx], _y[idx], _z[idx]); } } float doSomething1(ref VecArray array) { return array[0].x * 2.0; } float doSomething2(ref VecArray array) { return array._x[0] * 2.0; } ============================================== doSomthing1: float example.doSomething1(ref example.VecArray): push rax cmp qword ptr [rdi], 0 je .LBB6_4 cmp qword ptr [rdi + 16], 0 je .LBB6_4 cmp qword ptr [rdi + 32], 0 je .LBB6_4 mov rax, qword ptr [rdi + 8] movss xmm0, dword ptr [rax] addss xmm0, xmm0 pop rax ret .LBB6_4: lea rsi, [rip + .L.str.1] mov edi, 11 mov edx, 12 call _d_arraybounds PLT =============================================== doSomthing2: float example.doSomething2(ref example.VecArray): push rax cmp qword ptr [rdi], 0 je .LBB7_2 mov rax, qword ptr [rdi + 8] movss xmm0, dword ptr [rax] addss xmm0, xmm0 pop rax ret .LBB7_2: lea rsi, [rip + .L.str.1] mov edi, 11 mov edx, 23 call _d_arraybounds PLT
Dec 27 2020
parent reply Dennis <dkorpel gmail.com> writes:
On Sunday, 27 December 2020 at 17:01:47 UTC, claptrap wrote:
 It wont work with ref returns, and performance in release 
 builds.
What if you used pointers instead? Maybe it can be optimized when you make opIndex return a structure like this:
 struct VecArrayIndex {
     VecArray* vecArray;
     size_t index;
     ref float x() {return vecArray.x[index];}
     ref float y() {return vecArray.y[index];}
 }
I'm not trying to annoy you with awkward workarounds, I see what you're going for. I just really don't want operator overloading to become more complex than it already is. It's already confusing having opIndex, opSlice, opSliceUnary, opAssign, opIndexAssign, opIndexOpAssign, opIndexUnary etc. And then there's still some cases missing that the compiler uses for internal types that can't be expressed in library types: - multiple array concatenation without multiple re-allocations, e.g. `a ~ b ~ c` gets rewritten to a single call. - for associative arrays, opIndex followed by opIndexAssign, e.g. for an `int[string][string]` doing `aa["newKey"]["anotherNewKey"] = 1` does not give a range violation for `aa["newKey"]`. So there's good rationale for adding solutions to that so library solutions can replace compiler magic. Now you propose, opIndexMember, but that's just the beginning of even more possible combinations: next you'll need opIndexMemberAssign, opIndexMemberOpAssign, and who knows what. So that's why I want to fix / improve operator overloading without adding any more cases if it can be helped.
Dec 27 2020
parent claptrap <clap trap.com> writes:
On Sunday, 27 December 2020 at 23:06:47 UTC, Dennis wrote:
 On Sunday, 27 December 2020 at 17:01:47 UTC, claptrap wrote:
 It wont work with ref returns, and performance in release 
 builds.
What if you used pointers instead? Maybe it can be optimized when you make opIndex return a structure like this:
 struct VecArrayIndex {
     VecArray* vecArray;
     size_t index;
     ref float x() {return vecArray.x[index];}
     ref float y() {return vecArray.y[index];}
 }
That gives identical codegen to just accessing the arrays directly, so codegen is as good as it can be!
 I'm not trying to annoy you with awkward workarounds, I see 
 what you're going for. I just really don't want operator 
 overloading to become more complex than it already is. It's 
 already confusing having opIndex, opSlice, opSliceUnary, 
 opAssign, opIndexAssign, opIndexOpAssign, opIndexUnary etc. And 
 then there's still some cases missing that the compiler uses 
 for internal types that can't be expressed in library types:
To be honest I expected a bit more enthusiasm for the idea but Im not going to be upset or lose any sleep over it. I already had a work around, just that workarounds are not solutions, they avoid the problem rather than solving them. So I though it would be a nice addition to make SOA vs AOS easier to implement and easier for the compiler to produce optimal code. I hadnt thought of the consequences of it spawning other op overloads.
Dec 28 2020
prev sibling next sibling parent reply sighoya <sighoya gmail.com> writes:
On Sunday, 27 December 2020 at 14:46:43 UTC, claptrap wrote:
 In some cases a structure of arrays is more desirable than an 
 array of structs, usually for performance reasons, but maybe 
 you want tight packing or something else, so I have been 
 thinking a while about proposing the following...
What about this: ``` import std.math; struct Struct { float[] x; float[] y; float[] z; this(float[] x, float[] y, float[] z) { this.x=x; this.y=y; this.z=z; } float[3] opIndex(int index) { return [x[index],y[index],z[index]]; } } float x(float[3] array) { return array[0]; } float y(float[3] array) { return array[1]; } float z(float[3] array) { return array[2]; } float magnitude(float[3] array) { return sqrt(sqr(array[0])+sqr(array[1])+sqr(array[2])); } float sqr(float x) { return x*x; } int main() { import std.stdio; Struct s = Struct([1,2],[3,4],[5,6]); writeln(s[0].x); writeln(s[0].y); writeln(s[0].z); writeln(s[0].magnitude()); return 0; } ```
Dec 27 2020
parent claptrap <clap trap.com> writes:
On Sunday, 27 December 2020 at 17:34:44 UTC, sighoya wrote:
 On Sunday, 27 December 2020 at 14:46:43 UTC, claptrap wrote:
 In some cases a structure of arrays is more desirable than an 
 array of structs, usually for performance reasons, but maybe 
 you want tight packing or something else, so I have been 
 thinking a while about proposing the following...
What about this:
Codegen is better, but doesnt handle ref parameters, wont handle different types, maybe it's not just 3 floats, maybe a bool, or an enum too.
Dec 27 2020
prev sibling parent reply Per =?UTF-8?B?Tm9yZGzDtnc=?= <per.nordlow gmail.com> writes:
On Sunday, 27 December 2020 at 14:46:43 UTC, claptrap wrote:
 So basically if opIndexMember is defined for Foo then...

 foo[i].x

 Is rewritten to..

 foo.opIndexMember!("x")(i);
For an alternative solution see: https://github.com/nordlow/phobos-next/blob/master/src/nxt/soa.d No benchmarks yet, tough, but you're very welcome to add them.
Dec 28 2020
next sibling parent reply claptrap <clap trap.com> writes:
On Monday, 28 December 2020 at 20:15:27 UTC, Per Nordlöw wrote:
 On Sunday, 27 December 2020 at 14:46:43 UTC, claptrap wrote:
 So basically if opIndexMember is defined for Foo then...

 foo[i].x

 Is rewritten to..

 foo.opIndexMember!("x")(i);
For an alternative solution see: https://github.com/nordlow/phobos-next/blob/master/src/nxt/soa.d No benchmarks yet, tough, but you're very welcome to add them.
Hi, that works perfectly, and in godbolt, LDC -O3, identical codegen to just accessing the array directly.
Dec 29 2020
parent Per =?UTF-8?B?Tm9yZGzDtnc=?= <per.nordlow gmail.com> writes:
On Tuesday, 29 December 2020 at 10:41:44 UTC, claptrap wrote:
 Hi, that works perfectly, and in godbolt, LDC -O3, identical 
 codegen to just accessing the array directly.
Nice. Feel free to propose enhancements. The dependency import nxt.pure_mallocator : PureMallocator; should probably be turned into a template parameter.
Dec 29 2020
prev sibling parent reply Petar Kirov [ZombineDev] <petar.p.kirov gmail.com> writes:
On Monday, 28 December 2020 at 20:15:27 UTC, Per Nordlöw wrote:
 On Sunday, 27 December 2020 at 14:46:43 UTC, claptrap wrote:
 So basically if opIndexMember is defined for Foo then...

 foo[i].x

 Is rewritten to..

 foo.opIndexMember!("x")(i);
For an alternative solution see: https://github.com/nordlow/phobos-next/blob/master/src/nxt/soa.d No benchmarks yet, tough, but you're very welcome to add them.
Here's my own implementation which I dug out from an old project of mine from a few years back: https://gist.github.com/PetarKirov/a074073a12482e761a5e88eec559e5a8
Dec 29 2020
parent reply Petar Kirov [ZombineDev] <petar.p.kirov gmail.com> writes:
On Tuesday, 29 December 2020 at 22:33:01 UTC, Petar Kirov 
[ZombineDev] wrote:
 On Monday, 28 December 2020 at 20:15:27 UTC, Per Nordlöw wrote:
 On Sunday, 27 December 2020 at 14:46:43 UTC, claptrap wrote:
 So basically if opIndexMember is defined for Foo then...

 foo[i].x

 Is rewritten to..

 foo.opIndexMember!("x")(i);
For an alternative solution see: https://github.com/nordlow/phobos-next/blob/master/src/nxt/soa.d No benchmarks yet, tough, but you're very welcome to add them.
Here's my own implementation which I dug out from an old project of mine from a few years back: https://gist.github.com/PetarKirov/a074073a12482e761a5e88eec559e5a8
Comparing it to your implementation, the main advantage of mine is that it always takes (size_t.sizeof * 2) bytes, regardless of how many members the struct has since it uses a heap allocation for all of the arrays.
Dec 29 2020
parent Per =?UTF-8?B?Tm9yZGzDtnc=?= <per.nordlow gmail.com> writes:
On Tuesday, 29 December 2020 at 22:39:48 UTC, Petar Kirov 
[ZombineDev] wrote:
 Comparing it to your implementation, the main advantage of mine 
 is that it always takes (size_t.sizeof * 2) bytes, regardless 
 of how many members the struct has since it uses a heap 
 allocation for all of the arrays.
Lots of goodies there. Thanks
Dec 30 2020