August 05, 2021
On Thursday, 5 August 2021 at 16:46:53 UTC, H. S. Teoh wrote:
> [snip]
>
> In C++, structs also support OO-style inheritance, and upcasting / downcasting works the same way as in classes.  So I suppose it could be made to work.
>
> [...]

Makes sense. Thanks.
August 06, 2021
On Thursday, 5 August 2021 at 17:44:48 UTC, H. S. Teoh wrote:
> On Thu, Aug 05, 2021 at 05:24:56PM +0000, IGotD- via Digitalmars-d wrote: [...]
> Since the stack is almost constantly being accessed in the course of a program's execution, it remains in cache and so is much more cache-friendly than dereferencing a pointer to the

This raises the question how does this apply to a CPU that has a L1 cache of 512KiB, a L3 of 4MiB for a program with a stack size of 8MiB ?
As far as I'm aware stack memory is residing on the heap so it will need to be allocated on program start up.
So in theory...
if we were pre-allocating a chunk of memory on the heap at program startup and used that to store our data (like on a stack), the cost of allocation would have been paid, once we need to use it, and would only have to be done once. "Deallocation" cost for data using this "stack" should be getting pretty close to that of *the* stack (which is basically a subtraction). Deallocation cost for the block of heap memory on program termination doesn't matter.
In practice the "stack" would probably be closer to a pool and memory management a bit more involved than an addition/subtraction.

A cache line is usually much smaller than L1 at just a few data words. So once the pre-fetcher is set up, and the memory in question is residing in L1, there shouldn't be a difference anymore.

Therefore I would reason that utilizing cache line bandwidth efficiently is important and whether the data resides on the heap or stack is secondary (i.e. what a struct (doesn't) contain is more important than where it's stored).

August 06, 2021

On Friday, 6 August 2021 at 12:27:55 UTC, wjoe wrote:

>

So in theory...
if we were pre-allocating a chunk of memory on the heap at program startup and used that to store our data (like on a stack), the cost of allocation would have been paid, once we need to use it, and would only have to be done once. "Deallocation" cost for data using this "stack" should be getting pretty close to that of the stack (which is basically a subtraction). Deallocation cost for the block of heap memory on program termination doesn't matter.
In practice the "stack" would probably be closer to a pool and memory management a bit more involved than an addition/subtraction.

A cache line is usually much smaller than L1 at just a few data words. So once the pre-fetcher is set up, and the memory in question is residing in L1, there shouldn't be a difference anymore.

Therefore I would reason that utilizing cache line bandwidth efficiently is important and whether the data resides on the heap or stack is secondary (i.e. what a struct (doesn't) contain is more important than where it's stored).

The thing is, you are already forced to use the "real" stack if you want to take advantage of the CPU's call and ret instructions. Since you're going to be accessing that region of memory anyway, you might as well store your other "hot" data nearby.

That said, there are languages that use separate stacks for return addresses and parameter passing, like Forth. Doing so can be useful on embedded systems with highly restrictive stack-size limits, since it allows you to keep the size of the "real" stack small.

August 06, 2021
On Fri, Aug 06, 2021 at 12:27:55PM +0000, wjoe via Digitalmars-d wrote:
> On Thursday, 5 August 2021 at 17:44:48 UTC, H. S. Teoh wrote:
[...]
> > Since the stack is almost constantly being accessed in the course of a program's execution, it remains in cache and so is much more cache-friendly than dereferencing a pointer to the
> 
> This raises the question how does this apply to a CPU that has a L1 cache of 512KiB, a L3 of 4MiB for a program with a stack size of 8MiB ?

The stack is called the stack, precisely because you generally only access it at the top (or near the top). You generally do not access random parts of it, esp. not the parts near the bottom; most accesses are expected to be near the top.  That's the area of the stack that would tend to be in the cache.


> As far as I'm aware stack memory is residing on the heap so it will need to be allocated on program start up.

I think you're confusing the terminology here.  "Stack" refers to an area of a program's memory that the OS allocates specifically to be used as the runtime stack, the usage pattern of which is expected to be FIFO (first-in, first-out). The "heap" is an additional area (or areas, depending on the implementation of a language's runtime) designated for non-FIFO, ad hoc memory allocation (malloc/free, GC, etc.).

In modern OSes, the stack is mapped to its own range of addresses in the program's address space, which, depending on OS settings, may grow as program recursion level increases.


> So in theory...
> if we were pre-allocating a chunk of memory on the heap at program
> startup and used that to store our data (like on a stack), the cost of
> allocation would have been paid, once we need to use it, and would
> only have to be done once. "Deallocation" cost for data using this
> "stack" should be getting pretty close to that of *the* stack (which
> is basically a subtraction).  Deallocation cost for the block of heap
> memory on program termination doesn't matter.
[...]

That's missing my point.  My point was that the runtime stack is cache-friendly because it's always "hot": the program is practically constantly accessing it: every function call, every `return`, every access of local variables touch the stack, so it practically never leaves the cache.  Whereas a user-allocated stack on the heap may be evicted from the cache if it hasn't been accessed in a while.


T

-- 
What's a "hot crossed bun"? An angry rabbit.
August 07, 2021

On Thursday, 5 August 2021 at 17:24:56 UTC, IGotD- wrote:

>

I never understood the benefit of structs are value types. In what use case would you like to pass the value of a struct rather than a reference. The struct will be copied on the stack which is inefficient so in 99% of the cases you would like to use the reference, especially when the parameter is const. If you need a copy the called function could be responsible for that.

Could someone explain the benefit of this "glorified int".

(0) Small structs with one or two small, simple data members can usually be passed in via registers, just like basic types (int, float, pointers, etc.). This enables efficient custom number, pointer, slice, enum, bit flag, etc. types.

For example, on x86-64 Complex, below, is passed to f in registers:

struct Complex {
    float r, i;
}

float f(const(Complex) x) {
    return x.r * x.i;
}

(1) Forcing all custom aggregate types to be reference types causes aggregate composition to introduce pointless additional indirections, and seriously complicates initialization.

For example, suppose we want to make a third type by composing these two:

struct A {
    int x;
}
struct B {
    int y;
}

If A and B are value types, it looks like this:

struct C {
    A a = A(3);
    B b = B(7);
}

The default value can be pre-computed at compile time, and initialization is just a blit of C.init into the target address. Each instance of C requires 8 bytes.

But, if A and B were reference types, it would look like this:

struct C {
    A a = null;
    B b = null;

    this() {
        a = new A(3);
        b = new B(7);
    }
}

Initialization now requires two allocations, and each instance of C requires 16 bytes + at least 4 bytes for the new A and at least 4 bytes for the new B. I say "at least" 4 bytes because the true minimum heap allocation size for a general-purpose allocator is usually around 16 bytes, and there is some non-trivial overhead for the allocator's internal book-keeping, too.

(2) The fact that class values exist, but have no formal type in D, requires tons of special code to handle them correctly in certain low-level meta-programming tasks. I'm seriously considering banning class from my current project entirely so that I can simplify my custom memory management scheme.

August 07, 2021
On Thursday, 5 August 2021 at 17:24:56 UTC, IGotD- wrote:
> I never understood the benefit of structs are value types. In what use case would you like to pass the value of a struct rather than a reference.

Functional programming requires you to conceptually pass by value, then passing it by reference becomes an optimization. But in D this is lost since you also can pass by reference. So there is no advantage to having the struct/class split.

August 07, 2021
On Saturday, 7 August 2021 at 06:05:02 UTC, Ola Fosheim Grøstad wrote:
>
> Functional programming requires you to conceptually pass by value, then passing it by reference becomes an optimization. But in D this is lost since you also can pass by reference. So there is no advantage to having the struct/class split.

It's an interesting aspect of language design which seems to proliferate almost all imperative languages despite the main use is not functional programming. Rather having the compiler optimizing the parameter passing (which some do underneath anyway), default is copy and it has been like that for decades. Not sure what the historical impact is, maybe that early CPUs had few registers and the stack was the only choice.

In the case of functional programming, I would just let the programmer manually copy the struct inside the function body as no assumption can be made if the struct is passed as reference (pointer) or in registers, or just have a keyword that forces the struct to be copied on stack.
August 07, 2021
On Friday, 6 August 2021 at 14:18:30 UTC, H. S. Teoh wrote:
> On Fri, Aug 06, 2021 at 12:27:55PM +0000, wjoe via Digitalmars-d wrote:
>> [...]

This explains what I was wondering about for quite a while now. Thanks!
August 07, 2021
On Wednesday, 4 August 2021 at 16:45:27 UTC, H. S. Teoh wrote:
> On Wed, Aug 04, 2021 at 03:13:07PM +0000, jmh530 via Digitalmars-d wrote:
>> On Wednesday, 4 August 2021 at 14:32:17 UTC, bachmeier wrote:
> [...]
>> > It's difficult to beat the simplicity of alias this. Even if something cam be done with mixin templates, that's a lot of overhead vs `alias x this;`. I can't say I use mixin templates very often, but am skeptical that there's no way to misuse them.
>> 
>> I'm sympathetic to this.
>> 
>> I think if you got rid of alias this, then you would end up needing something else to accomplish some form of subtyping relationship for structs beyond just composition. For instance, the struct below can be used anywhere an int can be used. Without alias this, you either need to pull out that x value each time it is used in a function or you need to write overloads for all the functions you want to use Foo with like an int. Alternately, you can modify those functions to become generic and handle things that are like an int, but then what if you don't control those functions? It's just so simple.
>> 
>> ```d
>> struct Foo {
>>     int x;
>>     alias x this;
>> }
>> ```
>
> This is exactly why I used to love alias this. It lets me cowboy a wrapper type into code that wasn't originally intended to handle it, and it works wonderfully with minimal code changes.  I used to do this all the time in my code, and it let me get away with quick fixes that for the most part worked well.
>
> Unfortunately, the long term effect of this sort of hackish solution is a gradual degradation of maintainability and code readability. After a while, when new code needs to be added, it's not clear whether I should use type X, or wrapper type Y with alias this X, or wrapper type Z with alias this Y (chained `alias this` used to be my favorite trick).  It became context-dependent -- if I needed special functionality provided by wrapper type Y, then I'd use Y; if Z provided something I needed at the time then I'd use Z.  But soon I find out that I need *both* Z and Y in the same function, so my code started to get cluttered with interconversions between these wrappers.  Soon I had to invent yet another wrapper type that encompasses *both* Z and Y just to get all the data I need in one place. Which in turn leads to further such issues later down the road.
>
> Worse yet, (no) thanks to `alias this`'s super-convenient implicit conversions, half of the code looks like it takes Y but actually receives only X -- it's not immediately obvious because Y implicitly converts to X. So when I need to propagate Y's functionality down into code expecting only X, I find myself in a bind. And unlike actual OO polymorphism there is no equivalent in `alias this` to an upcast: once Y decays to X you cannot get Y back.  Which means that now, the code that originally received only X had to be revised to receive Y.  Also, code that receives X have no obvious connection to Y: there is no class hierarchy that you can look up to learn possible relationships between class X and class Y -- X can be the target of an implicit `alias this` conversion of literally *any* type anywhere in the program. It's highly unstructured subtyping that hurts long-term code readability and maintainability.
>
> Taking a step back from this cascading complexity, I realized that had I *not* used alias this, I would have been forced to consider how to encapsulate the functionality of X, Y *and* Z in a single common type (or a proper class hierarchy) instead.  It would have been much less convenient, but in the long run it would have improved code quality: instead of the above spaghetti involving shuffling the same danged data between X, Y, and Z, trying to figure out which type to accept and where implicit conversions are happening, there would have been a single unified type that everyone accepts.  There would be no question which (wrapper) type to use because there would be no wrapper types.  And there would be no spurious implicit conversions to trip you up when reading the code.  The code would be cleaner and more consistent -- instead of various modules accepting different variations on X, Y, and Z, there would be a single type that serves as common currency between all code, eliminating a lot of needless complexity and implicit conversion messiness.
>
> This is why, even though I loved alias this (and still do to some extent), I have come to realize that in the long term, it lies more on the negative side of the scale than the positive.
>
>
> T

Speaking of weird edge case. This code compiles.
struct TIntStatic
{

static int mX;

static @property int x() { return mX; }
static @property void x(int v) { mX = v; }

alias x this;
}
alias t = TIntStatic;
t = 5; // According to Walter bright this shouldn't compile, yet it does.
1 2 3
Next ›   Last »