5 ms·
Span and ref-like types enable massive changes to the way that memory is managed in C#. You can absolutely write almost GC-less code. I have been tinkering with
by algorithmsRcool 2y ago
Span and ref-like types enable massive changes to the way that memory is managed in C#. You can absolutely write almost GC-less code. I have been tinkering with a toy no-GC database engine in C# based on Direct I/O and some object pooling. I have been amazed at how far i can get before resorting to GC heap allocations
- DeathArrow 2y agoAnything that uses classes and interfaces will be memory managed by the GC. So instead of using lists, dictionaries, IEnumerable, you will have to roll your own. It would be better if the GC can be turned off with a switch and just add a delete operator to manually free memory.
- neonsunset 2y agoGiven that ref structs can now be generic arguments and cannot be boxed - you have more ways to enforce that no boxing occurs at compile-time. It is true that you have to roll your own collections, but even dispatching on interfaces by making them generic constraints (which is zero-cost) instead of boxing is a good start. 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.
- DeathArrow 2y agoI really mean using existing stuff, without rolling your own: List<int> nums = [1, 2, 3, 4]; //do stuff with nums Delete(nums);
- neonsunset 2y agoOkay, I see where you are coming from. This is a common ask, but it works against the principles that make generational GCs performant. You can't "delete" an object from the heap, because dead objects are not deallocated. Instead, live objects are preserved and moved to an older generation, with memory now occupied by only dead objects made available for subsequent allocations immediately. 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-K8Y-wYcDgDXKhfs6OT8fFQtVm https://www.youtube.com/watch?v=8i1Nv7wGsjk&list=PLpUkQYy-K8...
- DeathArrow 2y ago> Okay, I see where you are coming from. This is a common ask, but it works against the principles that make generational GCs performant. 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."
- whizzter 2y agoAnd that'd totally break down as soon as some underlying class does something you didn't expect. C++ RAII patterns and Rust's ownership systems are required for a very good reason (that the GC sidesteps but also makes all code dependent of), the NVec further up in the thread works because it's an explicit abstraction.
- pjmlp 2y agoUse the stuff from Marshal and OS interop then, there are even malloc/free variants. Also there is C++ for that, if the goal is to use C# as C++.
- zigzag312 2y agoThis looks intriguing. Is there anywhere I could see more details about this?
- neonsunset 2y agohttps://github.com/neon-sunset/project-anvil/blob/master/Sources/Core/NVec.cs https://github.com/neon-sunset/project-anvil/blob/master/Sou... 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.
- zigzag312 2y agoThank you! It'll be fine learning resource.
- ComputerGuru 2y ago> Given that ref structs can now be generic arguments I missed this development! That was a big pain working with ref structs when they first came out.
- algorithmsRcool 2y agoRef-structs can also implement interfaces now too. The C# compiler team has been really delivering in this space the last few iterations
- algorithmsRcool 2y ago>Anything that uses classes and interfaces will be memory managed by the GC... 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
- recursive 2y ago> constrain those interfaces to be structs or ref-structs How? I know of constraints on generic type parameters, but not how to do this. A cursory search is unhelpful.
- neonsunset 2y agoI think the comment just meant using generic constraints with structs. 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).
- yarg 2y ago> It would be better if the GC can be turned off with a switch and just add a delete operator to manually free memory. 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.