Thanks for all the good discussion on this topic!
After reading these posts, A refinement of my original question is, "Is the proposed destructor based memory management, as described, intrinsically slower than the GC, or is it just a matter of implementation and engineering effort?"
This is obviously not a cut and dry question. There are a lot of factors, but it sounds like the biggest intrinsic issue is the escape analysis / pessimistic destructor insertion.
This is obviously a hard problem. It is the same kind of issue that Rust tries to solve with the borrow checker, and is at the heart of the philosophical divide between GC and non-GC approaches to memory management.
If I can get a unified solution for resource management that costs 5-10% performance (before I optimized it to use custom allocators etc.) I'm willing to pay the price. Others may not.
Correct me if I'm wrong here: Ideally some better static escape analysis algorithm would solve the performance costs of the destructor runtime, but, it seems that much of those costs can be worked around by using custom allocators. Of course this is not nearly as automatic or magical as a GC, but it does simplify the language and makes the runtime semantics much more consistent in various ways.
Destructors make it much easier to implement and utilize custom allocators in general, which I really appreciate. But, I am biased.
The tracing GC could be replaced with atomic reference counting with a cycle collector. That's of course still a GC but one that plays as nice with deterministic destruction as possible.
Rust has pretty much exactly this with it's RC<T> type, so that is definitely a reasonable compromise that has precedent.
@Udiknedormin: afaik, Rust has nothing like a tracing GC, so I'm not sure what you are referring to. The RC<T> type is the closest thing Rust has to a GC, is that what you are referring to?
- implementation in C - https://github.com/rmind/libqsbr
- in C++ with accompanying blog:https://blog.scrimpycat.io/simple-lock-free-epoch-memory-reclamation and https://github.com/ScrimpyCat/CommonC/blob/master/CommonC/EpochGarbageCollector.c
- in rust by the author of my OP: https://github.com/crossbeam-rs/crossbeam/tree/master/src/epoch
- Goldmine of papers: https://kukuruku.co/post/lock-free-data-structures-the-inside-memory-management-schemes/
@bpr Not in the core (or std, I guess, but's more realistic). But I never said I was talking about the core.
@rayman22201 I was referring to a GC library for Rust. I can see @mratsim already mentioned one example (there are more, if my memory serves me well). There are two main reasons for it to be useful:
- it's much simpler to use than Rc<T>
- you don't need locks, locks can be nasty
- if you treat Rc<T> as a GC (as suggested in the quotes you mentioned), it's not really that spectacularly efficient GC
I eventually want to have a simpler, more consistent language that does not provide every feature under the sun.
You never cease to amaze me. xD But I'm glad you came to this conclusion.
I don't really have experience with GCs but Nim's GC seems nice. I'm quite surprised you'd like to replace it with refcounting with a cycle collector, which, as far as I know, isn't particularly elegant, smart or efficient.
@Udiknedorm: The biggest problem with the current GC is that it doesn't support shared memory (though, I don't know why we can't do something like Go's channels).
To me, these changes seem like an effort to reasonably support programs that don't want to rely on the GC.
@Varriount It doesn't? I was pretty sure there was something called shared heap... Well, whatever. I think GC is much better for functional languages, RAI seems better for stateful ones.
Oh, by the way --- I didn't really try it, but I guess idiomatic Nim programs could bring problmes on some microcontrollers. I once wrote a custom language for a really simple microcontroller. I actually struggled to fit communication, parsing, bytecode, user code, VM with a GC. I'm not sure if I would succeed if not for how trivial the GC was. Most of the language was stack-based, I needed a GC to manage cleaning unreachable bytecode. And in general, I don't think it's a really great idea to do dynamic allocations on small microcontrollers.
Many microcontroller only have a stack so you just can't use heap .
Also @Varriount, @Udiknedormin today you can do:
type SharedArray[T] = object dataRef: ref[ptr T] data*: ptr UncheckedArray[T] len*: int proc deallocSharedArray[T](dataRef: ref[ptr T]) = if not dataRef.isNil: deallocShared(dataRef) dataRef = nil proc newSharedArray[T](size: int): SharedArray[T] = # Note the finalizer to call the GC when needed new(result.dataRef, deallocSharedArray) # Note the shared memory allocation let address = cast[ByteAddress](allocShared0(sizeof(T) * size)) result.dataRef = cast[ptr T](address) result.data = cast[ptr UncheckedArray[T]](address) result.len = size proc ``[T](a: SharedArray[T], idx: int): T = assert idx >= 0 and idx < a.len # bounds-checking a.data[idx] proc ``[T](a: var SharedArray[T], idx: int): var T = assert idx >= 0 and idx < a.len # bounds-checking a.data[idx] proc `=`[T](a: var SharedArray[T], idx: int, v: T) = assert idx >= 0 and idx < a.len # bounds-checking a.data[idx] = v var a = newSharedArray[int](5) a = 10 echo a # 0 echo a # 10
@mtrasim I've already said it's a bad idea (even on many architectures on which it's possible). And even if you don't really have a heap, it doesn't mean you couldn't use dynamic memory if a language supports custom allocators. You can provide a memory pool on a stack, that's what I actually did in the VM I mentioned.
allocShared, just like I said. But can I make sure about why you use both dataRef and data fields? data is a public pointer to the data so I guess you'd like to have raw access to the fixed location (what a constant pointer in C would do). But then, as far as I know, you can reassign data field to another location? I guess an inlined getter would be better. The same for len as you can definitely reassign it, breaking all the assertions (in general case you'll end up desynchronizing copies).
Also: a deep copy will make a copy of the ref object, i.e. a ptr. So the _actual data won't be copied and so you end up with two semantically different objects which point to the same data. Both can possibly try to deallocate it then. Not a really good idea, as not only this object can't be properly deepCopied but does it quite wrong. You'd need to provide a separate type (not just ptr T) instead so you could overload deepCopy. As you can see, it's not as trivial as your example.
Also: I don't think you'd like to reimplement seq and all the other standard containers for them to use shared heap? Especially considering it being non-trivial, as I proved to you?
dataRef to have the deallocation of the data managed by Nim GC.
data for normal use through foo (thanks to UncheckedArray). Yes data is public because in my use case this is only used internally, and getter/setter would just add boilerplate for no gain. Same thing for len.
Regarding copy, just think of it as an object with reference semantics, any ref object will have the same issue.
My example was just to show that you can have the GC manage shared memory if you manually allocate it and use a finalizer for deallocation.