It would be better if the GC can be turned off with a switch and just add a delete operator to manually free memory.
As for delete operator, 'dispose' works well enough. I have a toy native vector that I use for all sorts of one-off tasks:
// A is a shorthand for default allocator, a thin wrapper on top of malloc/realloc/free
// this allows for Zig-style allocator specialization
using var nums = (NVec<int, A>)[1, 2, 3, 4];
nums.Add(5);
...
// underlying pointer is freed at the end of the scope
It is very easy to implement and I assume C and C++ developers would feel right at home, except with better UX.This retains full compatibility with the standard library through interfaces and being convertible to Span<T>, which almost everything accepts nowadays.
System-provided allocators are slower at small allocations than GC, but Jemalloc easily fixes that.
List<int> nums = [1, 2, 3, 4];
//do stuff with nums
Delete(nums);
In addition, objects that hold references to other objects internally would need an implementation that would allow to traverse and recursively free references in a statically understood way. This gets nasty quick since a List<T> can hold, let's say, strings, which may or may not have other locations referring to them. Memory safety goes out of the window for dubious performance wins (not even necessarily, since this is where GC has better throughput).
I can recommend watching the lectures from Konrad Kokosa that go into the detail how .NET's GC works: https://www.youtube.com/watch?v=8i1Nv7wGsjk&list=PLpUkQYy-K8...
In my comment I already suggested a context where GC can be turned off. I said: "It would be better if the GC can be turned off with a switch and just add a delete operator to manually free memory."
Also there is C++ for that, if the goal is to use C# as C++.
This really is a PoC. You might get better results by using snippets as the inspiration for rolling something tailored to your specific use-case.
I missed this development! That was a big pain working with ref structs when they first came out.
Yes and no. Yes, almost all of the standard library collection are allocation heavy and it is still the dominate pattern in C#, so if you want to avoid the GC you need to avoid these and resort to building your own primitives based on Memory/Span. Which sucks.
However, you can use interfaces in a no GC world since you can constrain those interfaces to be structs or ref-structs and the compiler will enforce rules that prevent them from being boxed onto the GC heap.
Also of recent note, the JIT can now automagically convert simple gc-heap allocations into stack allocations if it can trivially prove they don't escape the stack context.
> It would be better if the GC can be turned off with a switch and just add a delete operator to manually free memory.
It is a little know fact that you can actually swap out the GC of the runtime. So you could plug in a null implementation that never collects (at your own peril...)
As for a delete operator, you can just roll your own struct based allocation framework that uses IDisposable to reclaim memory. But then you need to deal with all the traditional bugs like use-after-free and double-free and the like.
For me, I think low-gc is the happy medium. Avoid the heap in 99% of cases but let the GC keep things air tight
How?
I know of constraints on generic type parameters, but not how to do this. A cursory search is unhelpful.
e.g.
interface Foo {
int Calculate();
}
static void CalculateThing<T>(T impl)
where T: Foo {
var num = impl.Calculate() * 2;
Console.WriteLine(num);
}
Here if you pass a struct that implements 'Foo', 'CalculateThing' will be monomorphized and the dispatch will be zero-cost, same as in Rust.You can apply additional constraints like `where T: struct` or `allows ref struct`. The last one is a new addition which acts like a lifetime restriction that says that you are not allowed to box T because it may be a ref struct. Ref structs are for all intents and purposes regular structs that can hold so-called "managed references" aka byrefs, which have syntax 'ref T', which is discussed in detail by the article this submission links to (ref structs can also hold other ref structs, you are not limited in nesting, but you are limited in cyclicality).
I forgot that there is built in support for this model using the MemoryManager<T> class [0]. A memory manager is an abstract class that represents a block of other memory, including possibly unmanaged memory. It implements IDisposable already so you can just plug into this.
The Memory<T> struct can optionally internally point to a MemoryManager instance allowing you to plug your perfered style of allocation and freeing of memory into parts of the framework.
There is a little irony that a MemoryManager<T> is itself a class and therefore managed on the gc-heap, but you can defeat this by using ObjectPool<T> to recycle those instances to keep allocation count steady state and not trigger the GC.
I have used this before (in the toy database i mentioned earlier) to allocate aligned blocks of unmanaged memory.
[0] https://learn.microsoft.com/en-us/dotnet/api/system.buffers....
How do you do this? Just so I can have another tool in my tool shed. Googling got me to an archived repo on GitHub with a sample GC - which is enough but Wonder if there’s something off the shelf.
In java land, the Epsilon GC (a do nothing GC) enables a pattern that’s handy in perf test jobs in CI pipelines occasionally for some projects (I.e. run with epsilon but constrain max memory for the process - ci builds will fail if memory usage increases)
I am not aware of any production grade replacement GCs for .NET out there currently
This breaks the fundamental assumptions built into pretty much every piece of software ever written in the language - it's a completely inviable option.
Incorporating a borrow checker allows for uncollected code to be incorporated without breaking absolutely everything else at the same time.