Ideas for improving better control over memory ownership and deallocation?

Hi,

I would like to request ideas/proposals for better control over memory ownership and deallocation that fit cleanly into the language. Unless ownership can be controlled, the runtime is responsible for deallocation, which usually involves Major GC, which causes freezes.

A language that has both a garbage collected, and support for managing memory yourself. That would be a first - interesting idea.

Though, I'm not sure if it's even feasible to do? I don't know the internal details about how different browsers implement their garbage collector, but it's possible that having "unmanaged" and "managed" memory sharing the same heap could add a lot of complexity to garbage collectors, which ultimately makes the garbage collector run even slower. And putting the unmanaged memory in a separate heap could take up a lot of extra memory.

I don't actually know if that's the case or not - maybe one of the really smart people on this form could correct me.

A language that has both a garbage collected, and support for managing memory yourself. That would be a first - interesting idea.

A language that has both garbage collected, and support for managing memory yourself safely would definitely be a first. JS must be a safe language.

Dlang has the unsafe variant. You can manually deallocate GC memory.

Here is a strawman demonstrating it's possible. (This isn't a serious proposal, that's efficient or usable or complete, just a way to explain how it is possible.)

"Very private class variables" (Alternate name: slots)
Syntax: "##"

class Cls {
   constructor() {
       this.##foo = {};
   }
}

Accessing the foo property (e.g. this.##foo) returns a new Proxy-like object that holds a WeakRef and uses it to access the object. The reference may be passed outside the object, but it's a weak reference. There is a language-level that Cls safely owns the sole strong reference to ##foo.

The object that contains the very private variable also owns it, because no other object can get a strong reference to it. The very private variable is deallocated when the outer object owns it. So, when a Cls instance is deallocated, the engine may deallocate ##foo too. In addition to avoiding a separate garbage collection, it enables optimizations like putting foo near Cls in memory– which helps with CPUs because they fetch a cache line of memory at a time– or in certain engines, on Cls itself to remove a pointer access.

Ah, I think I understand - so in your example above, you wouldn't explicitly deallocate the "very private variable", the garbage collector would just know to deallocate it with the instance when the instance gets garbage collected, and this eases some burden on the garbage collector. Am I interpretting this correctly?

You can have a mixed approach when using Rust: gc - Rust

1 Like

Correct.

as a user who always forgets some closure somewhere a variable setting or method that would help explain where when and why something is or is not gc'd woudl be really helpful. Not sure how to do it. Perhaps it is a separate type of variable that is used when debugging. As I type this I am wondering if this could be done in an npm module? This is kind of related to your topic but would solve the problem so that the user knows where he is messing things up.

For memory leaks of that type, see Track Retaining Path. You should call %DebugTrackRetainingPath when there should be no more references to an object. (e.g. after a game entity dies and is removed)

This discussion is about how we can reduce GC pressure and make realtime apps pause less often.

1 Like

How about simply extending delete? Currently delete is only used to remove properties from an object. What if it was also allowed to deallocate objects on the spot?

let test = { alpha: 1, beta:2, gamma:3, delta: 4, epsilon 5 };
//Current use
delete test.beta;
console.log(test); // { alpha: 1, gamma:3, delta: 4, epsilon 5 };
//New use
delete test;
console.log(test); // undefined

Deleting a variable like that would dereference the object. If it's reference count hits 0, delete would walk the object's own property list, dereferencing any objects assigned. Should an object be encountered with no other references, that object also gets deleted. The variable holding the deleted reference is set to void 0 afterward. So the delete action acts recursively and immediately. Any object not submitted to delete is handed over to the garbage collector as normal when it goes out of scope. This would give the developer a large degree of control over the destruction of objects.

If there is objection for overloading delete for this use case, it could always be called dealloc. Creating such an ability will likely invite desire for "destructor" functions. Deny any such request.

1 Like

"if it's reference count hits zero" would pretty much force any engine to actually do reference counting. Also that doesn't work well with cycles. Additionally I guess that would be hard to specify, as the way how values stored is currently up to the engine. And what is actually the usecase for this? The most performant garbage collector is the one that never runs, so I don't see the usecase for forcing a deallocation.

That conflicts with with and global variables in non-strict code.

globalThis.foo = "bar"
delete foo
console.log(foo) // reference error

Perhaps there could be some sort of RefCounted primitive...

Reference counting isn't something everyone does, nor something the spec requires someone to do, and it's explicitly something the spec has intended to never mandate, so I doubt it.

It's not so much a conflict. It just requires a clarification. If the variable following delete is actually a property of an object, like in the case of with(x) or global variables which are actually globalThis.<x>, the normal delete operation is carried out.

var foo = 2;
delete foo; //same as delete globalThis.foo
var bar = { fu: 42 };
with(bar) {
  delete fu; //same as delete bar.fu
}
delete bar; //same as delete globalThis.bar
{
  let bar2 = { fu: 42 };
  with(bar2) {
    delete fu; //same as delete bar2.fu
  }
  delete bar2; //fully dealloc's bar2
}

To be clear, I don't care what the underlying method of tracking usage happens to be. The point is that if the reference being deleted is the last strong reference, then the memory is immediately de-allocated.

Oh - that's weird, never thought about this before:

globalThis.x = 3
const obj = { x: 2 }

with (obj) {
  console.log(x) // 2
  delete x
  console.log(x) // 3
  delete x
  console.log(x) // error
}

"with" is a funny beast.

It's also one of the most powerful tools in the language. Unfortunately, the weirdness of it makes it one of the least understood tools as well. While I don't agree with TC39 blocking it from strict mode, I can at least understand why they did it.

What if we had a RefcountedRealm? It is a Realm where all strong references are refcounted/cycle collected/deterministically memory management. Refcounting is not required, but memory management must be deterministic and low-latency is encouraged.