dotnet / dotNext

Next generation API for .NET
https://dotnet.github.io/dotNext/
MIT License
1.56k stars 119 forks source link

[API Proposal]: Add .ctor(Span<T> span) to MemoryOwner<T> #197

Closed CodingMadness closed 8 months ago

CodingMadness commented 8 months ago

Title.

I would like to have the same behaviour as does MemoryRental<T> but I need it to out live a func-stackframe so it should be a normal non-ref struct, AFAIK MemoryRental<T> offers you the option to pass a Span which is good, I need this exact for the MemoryOwner<T>.

sakno commented 8 months ago

MemoryOwner<T> is not a ref struct, therefore it can't have ref fields or fields of ref struct type. This is limitation of .NET GC. GC cannot track interior pointers located in the heap. Moreover, proposed change breaks backward compatibility. Use MemoryRental<T> instead. Probably, C# compiler prevents you from storing MemoryRental<T> in a field of regular class or struct. This is fine, because it may points to stack-allocated memory and this memory doesn't exist out of stackframe.

static MemoryRental<int> M()
{
   Span<int> span = stackalloc int[10];
   return new MemoryRental<int>(span); // how this should work?
}
sakno commented 8 months ago

In the same time MemoryOwner<T> is able to represent unmanaged memory allocated using NativeMemory class from .NET library. To do that, you can use UnmanagedMemoryAllocator class.

sakno commented 8 months ago

Copy method allows to make a copy of memory block and represent the copy as MemoryOwner<T> using any memory allocator.

CodingMadness commented 8 months ago

Okay, good to know

CodingMadness commented 8 months ago

MemoryOwner<T> is not a ref struct, therefore it can't have ref fields or fields of ref struct type. This is limitation of .NET GC. GC cannot track interior pointers located in the heap. Moreover, proposed change breaks backward compatibility. Use MemoryRental<T> instead. Probably, C# compiler prevents you from storing MemoryRental<T> in a field of regular class or struct. This is fine, because it may points to stack-allocated memory and this memory doesn't exist out of stackframe.

static MemoryRental<int> M()
{
   Span<int> span = stackalloc int[10];
   return new MemoryRental<int>(span); // how this should work?
}

Although you could allow it by [UnscopedRef] attribute over the respective function, to avoid the compiler error and call a Property: public Span<T> Data => new Span(_ptr, _count) where _ptr is a T* inside your MemoryOwner<T> because I could then pass an X-Amount to use to allocate on stack, and internally ofc you can check if the requested Memory would overflow and then pass that to unmanaged or rent from pool. take a look:

[UnscopedRef]   //------------->important, to allow the stackalloc expression!
private MemoryOwner<byte> _pool { get; }

public MyStruct(int amount)
{
         //I think you can check internally, with "StackAllocThreshold" constant, if I can do this, if I cant then the entire `amount` will be used to rent from pool or unmamaged, however the choice, but if it does not exceed stacklimit, just use stackalloc
        _pool = new(stackalloc byte[amount]);
}

public void DoWork() =>//do smth with _pool!
sakno commented 8 months ago

_pool = new(stackalloc byte[amount]);

That is completely invalid, stack memory doesn't exist out of the method. You can crash the entire program with SIGTERM in attempt to access the memory.

UnscopedRef

It is invented to instruct the compiler that the enclosing interior pointer escapes the marked method or getter. Nothing more.

CodingMadness commented 8 months ago

How then does it InlineArray(x) is it not the same stack space which is being allocated here with the same mechanics?

CodingMadness commented 8 months ago

Another related question, when would you prefer to allocate Unmanaged rather to just renting from the pool? (except the request for pooling memory is to huge)

sakno commented 8 months ago

How then does it InlineArray(x) is it not the same stack space

Do you mean this feature? If yes, it's just a macro for JIT/AOT compiler. It follows the same rules as regular struct with copy semantics:

[System.Runtime.CompilerServices.InlineArray(3)]
public struct Buffer
{
    private int _element0;
}

// it's the same as
[StructLayout(LayoutKind.Sequential)]
public struct Buffer
{
    private int _element0, element1, element2;
}

When you trying to return Buffer or store it in a field, the compiler produces copy-by-value of the entire value type. No magic here.

Unmanaged rather to just renting from the pool?

Depends on the use case. Memory pooling can be a source of frequent full GC. For instance, you trying to decode binary data to base64, but don't know the size of that binary data (even approximately). In this case, unmanaged memory is better because you don't need to wait for full GC to release the memory back to the heap, and the memory block is less likely to be reused. If you know the size of the binary data approximately (or const which is ideal case), then use memory pooling. Once again,

stackalloc[32]; // 32 is const, this is a good use case for stackalloc
stackalloc[variable]; // still fine, if you known the upper bound of 'variable' and it's checked somewhere above explicitly
stackalloc[a * b]; // bad if 'a * b' never checked for its bounds. In this case, replace stackalloc with the memory allocated in the heap

This is why the following pattern exists:

const int stackallocThreshold = 20;
var memory = size <=stackallocThreshold
  ? new MemoryRental<byte>(stackalloc byte[stackallocThreshold], size)
  : new MemoryRental<byte>(size);
CodingMadness commented 8 months ago

Gotcha. Thanks.