> * but if you have accepted the tradeoffs of using dynamic memory allocation then why not GC?*
That’s the thing: many do not use dynamic allocation at all. Did you know for instance that you could write an entire modern cipher suite using only a small (<300 bytes), strictly bounded stack allocation?
As for those who do use the "heap", many can do so with a strict stack discipline, so it’s not really a heap. Reason being, the environment has an extremely low call stack budget, so all bigger allocations happens in another stack, located in the heap.
Then there are arenas, which are much simpler to implement than a general allocator, tend to have less overhead (both space and runtime) than malloc() or a GC, though I reckon they can still run out of space. Still, less treacherous than uncontrolled use of malloc().
Most of all though, even if the compiler can do mighty static region analysis to minimise heap usage and GC overhead, heck, even perhaps reduce it to absolutely zero in some cases, there’s still the problem of the programming interface: how does the programmer know about stuff like max heap usage? I guess the compiler could tell them, but then how do they fix it if usage exceeds the limit, or is unbounded? The wouldn’t be any clear link from heap usage to the source code, or it would be so diffuse it would be hard to determine where to begin.
And of course, a GC’ed program would likely rely on many more pointers under the hood than one that manages its memory manually, which means significant space overhead even if the GC implementation itself is perfect.
You want me to write something for an ESP-32, it’s gonna be in something like C, Rust, or Zig.
Exactly. You don’t need GC if you don’t produce garbage. And since Go uses stack-based memory like C you can avoid producing garbage just the same.
And if you do need some kind of special memory allocation then you can do the same tricks you do with C. You still don’t have to use the built-in allocator.
All the bellyaching about GC is silly because you don’t need it in the first place. The cries are like saying C isn’t suitable for microcontrollers because its standard library includes malloc… Just don’t use it.
> You want me to write something for an ESP-32, it’s gonna be in something like C, Rust, or Zig.
Sure. The Go creators were explicit that Go is designed for building network systems. That doesn’t mean it is impossible to use for anything else, but it was optimized for a particular niche. You will feel more comfortable using languages designed for microcontrollers when programming for microcontrollers.
But it’s not GC you aren’t using that gets in the way. You don’t have to use every language feature just because it is there. Rust doesn’t become unusable for programming because it has a string type and you only need integers. Just don’t use it.
"Just don’t use it" is a valid argument, if it’s clear when you’re using it or not. It’s easy not to use `malloc()`: just don’t call it, and don’t call dependencies that call it. Same for Rust’s string: don’t construct strings, and don’t depend on stuff that does.
In some cases, avoiding heap allocation is just impossible. Take OCaml: last time I checked (over 10 years ago) there are two kinds of values: integers, and pointers to heap allocated objects. That’s basically it. Even floats are allocated on the heap (except when inside an array that’s the only optimisation). So unless you only do integer operations and avoid constructing any kind of data structure, you can’t avoid heap allocation.
Other languages are much more conservative than OCaml in this regard. That with value types and all. If they’re clear enough which constructs don’t trigger heap allocation, and the set of thereof is rich enough to allow significant work, then yes "just don’t use the GC" is a valid option. Not all languages (I mean implementations, but we can conflate the two in most cases) fulfil those two conditions. Maybe Go does.
> I mean implementations, but we can conflate the two in most cases
Perhaps there is something to be said about that within the space of all programming languages, but why would you conflate them in our discussion about Go given that having multiple implementations was established as a language design requirement? For many years the core Go team maintained two different Go compilers themselves to ensure that requirement was met, although these days it has turned its focus to one, relying on the community of other compilers to fulfill the other implementation requirements. It is impossible to have that conflation because there isn't just one implementation in which to conflate it with.
In practice, if you were going use Go on a microcontroller you would not use the same compiler as you would use if you writing a web server. There are different tradeoffs for different computing environments and so different compilers can focus on different machines. As we already discussed, one such tradeoff is in GC implementations. While it is fair to say that dynamic memory allocation is not ideal on microcontrollers to begin with, if you chose to go down that road and accepted GC to go with it you'd want a GC designed for microcontrollers, not a GC designed for handling web requests. Those needs are very different. Conflating languages and implementations straight up doesn't make any sense in the context of this particular discussion.
> In practice, if you were going use Go on a microcontroller you would not use the same compiler as you would use if you writing a web server.
Is that a fact? Are you telling me that today, there's a Go compiler that's best suited for web servers, and a different Go compiler best suited for micro-controllers? Could you name the micro-controller one?
> As we already discussed, one such tradeoff is in GC implementations.
The internals of the GC are less important than how they affect the language itself. When memory is very limited, I need predictable memory usage, so memory usage must be part of the language's semantics. If they're not, I can't use that language in a constrained environment, full stop.
At the very least, I need to know which subset of Go's language and standard library have predictable memory usage. If there is no such clear subset, then Go cannot reliably be used in a constrained environment.
> Are you telling me that today, there's a Go compiler that's best suited for web servers, and a different Go compiler best suited for micro-controllers?
No, I would have no reason to tell you that because, given what we're talking about, you would already know that. You couldn't possibly partipcate in good faith if weren't familiar with the Go ecosystem. This is such a strange comment.
First, I’m not familiar with the Go ecosystem. I have never written a single line of Go, and I learned in this thread that there was more than one compiler. Second, please don’t accuse me of engaging in bad faith. I’m not.
My question was genuine. What triggered it is when you wrote "in practice". I myself draw a sharp distinction between a language and its implementation, but the moment "in practice" gets thrown in, I have to assume we’re talking about existing implementations only. And I have to say, as unfamiliar as I am with the Go ecosystem, the existence of a compiler (somewhat?) tailored to embedded use, is deeply surprising to me.
That’s the thing: many do not use dynamic allocation at all. Did you know for instance that you could write an entire modern cipher suite using only a small (<300 bytes), strictly bounded stack allocation?
As for those who do use the "heap", many can do so with a strict stack discipline, so it’s not really a heap. Reason being, the environment has an extremely low call stack budget, so all bigger allocations happens in another stack, located in the heap.
Then there are arenas, which are much simpler to implement than a general allocator, tend to have less overhead (both space and runtime) than malloc() or a GC, though I reckon they can still run out of space. Still, less treacherous than uncontrolled use of malloc().
Most of all though, even if the compiler can do mighty static region analysis to minimise heap usage and GC overhead, heck, even perhaps reduce it to absolutely zero in some cases, there’s still the problem of the programming interface: how does the programmer know about stuff like max heap usage? I guess the compiler could tell them, but then how do they fix it if usage exceeds the limit, or is unbounded? The wouldn’t be any clear link from heap usage to the source code, or it would be so diffuse it would be hard to determine where to begin.
And of course, a GC’ed program would likely rely on many more pointers under the hood than one that manages its memory manually, which means significant space overhead even if the GC implementation itself is perfect.
You want me to write something for an ESP-32, it’s gonna be in something like C, Rust, or Zig.