In any case, the tradeoff you're making is that you have deal with memory. Even in a language like Rust, you need to care about references, ownership, struct vs heap and possibly even reference counting. You might even be bitten by "running out of memory" due to memory defragmentation.
In most cases, you don't need to care about these things in a GC'ed language, at the expense of larger memory usage and possibly noticable pauses.
GC has other drawbacks as well. The whole tracing workload (which still exists even in low-latency, concurrent GC's) messes up your locality of references and interacts badly with CPU- and OS-level caching and memory management. Plus the mutator part of your program is heavily constrained in how it can layout objects in memory, since the tracing GC must be enabled to select references to other objects unambiguously. It's a non-trivial drain on performance on memory-bandwidth limited workloads, which tends to be most of them these days.
Reference counting does away with most of these issues, and gives you deterministic finalization of all resources not just memory. Alternately, you can selectively use arenas to defer the freeing of some objects, while still being deterministic elsewhere.
Reference counting is a form of GC to use when performance doesn't matter. Performance never matters except where it does. But automated memory management without reference counting is usual practice in a modern non-GC language.
Freeing an array of objects only involves a series of calls to a deallocator when the array elements have pointers in them. That is typically unavoidable in Java, but not in non-GC languages.
Arena allocation, where memory in a subsystem is allocated using a specific allocator object, deallocation there is an in-line no-op, and all of subsystem memory is reclaimed en bloc at a chosen event boundary, is a common alternative where more control is needed. It is still not "manual memory management"; memory for objects is managed invisibly, and still without reference counting.
The usual advertising for GC is that it means you don't need to think about managing memory. The actual experience is that, where it matters at all, you have to think about it a great deal more.
Only if elements of the array are separately allocated (boxed) or have destructors.
Thus, it is incorrect to talk about multiple calls to a deallocator. You get just one for the whole array.
If I were to port the app to C#, I have no way of telling the C# GC to never pause the audio thread, and to only pause the GUI thread. I'm not sure if it's possible for a tracing GC to coexist with a never-paused thread which owns manually memory-managed types, and for the two threads to exchange manually freed or GC'd objects without FFI boilerplate, serialization, or copying. If it exists (and there's a GUI framework written in the GC portion of the language), let me know!
Admittedly manual memory management does mean a lot more things to worry about (ranging from manual memory management to lock-free wait-free programming) Personally I find deterministic lifetimes and single ownership which can be moved/swapped to be elegant. However, Qt's QObject system combines the practical disadvantages of manual freeing (having to track complex semantics in your head, and check whether each method call transfers ownership or not, with leaks or use-after-free if you get it wrong) with the inelegance of GC (pervasive aliasing and mutability, unclear ownership, a magical runtime-like system).
My point was that there are tradeoffs, which you admit. The comment i responded to indicated that there wasn’t one.
But the fundamental thing is that you still have to think about memory, memory layout will have to be considered during refactors, etc.
And "tradeoffs" is a misleading term to apply to a process that does not involve giving up anything in exchange for the benefit of pause-free, fully predictable operation.
And you do absolutely give up something with not using a GC: speed of development, much bigger teams can work on the project at the same time while not stepping as much on each other’s foot, etc. It’s not an accident that perhaps the majority of all software development is in managed languages, where it is not absolutely crucial to control the exact memory layout.
That is the advertising claim, not substantiated.
A more plausible explanation for "the majority ... in managed languages" is: it enables employing lower-skilled, thus more easily obtained, labor. It doesn't so much matter how fast development is. Such practices commonly split a simple job among five or more people (a "team") who, collectively, probably cost more than one more-skilled employee who could do it all in much less time, but who is hard to attract to do it at all, and anyway better used elsewhere.
And frankly, you are not getting ahead with your arbitrary gatekeeping on “who is a real programmer”.
The statement about enabling lower-skilled employers to be hired is pretty much false. Embedded development payed significantly less than most places that uses Java today (at least in Norway), and there's really no difference in the skill of the people I work with.
There's a _significant_ difference in the problem being solved though. The project I'm working at now is, code-wise, bigger by an order of magnitude. As is the problem domain (ticket-selling services, vs firmware of payment terminal in old job).
When doing embedded, I never had to care about racing conditions in threads or across several microservices. The embedded device was single-core anyways, and only talked to one server.
Dealing with memory has been replaced by handling concurrency. Of those I'd argue that the latter is significantly harder than the first. But both add to development time, as it becomes a core point in most design discussions.
In our case, we truly do not need to care about memory. If we run out of it, we either upgrade the instance or add another instance. The monthly hardware cost is lower than the hourly cost of a developer, anyways. Having one less thing to worry about decreases developer time, as there's one less thing to include in our designs, and one less thing to worry about going wrong.
Not sure if you can disable the GC entirely, but you can set the initial heap high enough that it won’t trigger for the lifetime of the process.
Taking a dogmatic view on an engineering trade-off isn't good engineering, and closes you off from exploring other, potentially better, ways of solving the problems you actually care about. (Which probably have nothing to do with the details of memory management)
Something tells me you wouldn't say "I prefer exactly no borrow checker [in Rust], and I don't think I'm giving anything up by skipping them".
(Comment was edited while I was writing a response, original comment I responded to above)
I'm glad to hear that you've taken the time to understand how GC work, and the trade-offs that languages make when use them.
I would be curious to learn more about what you're research as has shown with regards to using a GC in your problem space. It's always interesting to learn about areas of engineering that unique requirements.
A garbage collector can do that work concurrently.
CppCon 2016: Herb Sutter “Leak-Freedom in C++... By Default.”
https://www.youtube.com/watch?v=JfmTagWcqoE
Better making use of the best practices advised by Herb, otherwise those destructors are going to surprise you.
> It has been more than thirty years since a destructor surprised me. Herb was unlikely to have been programming at that time.
So apparently you have skills that top one of the major C++ community figures.
Being so, the C++ community at large would appreciate those valuable insights.
Same applies to the "program terminates before it runs out of memory" approach.
C++ has destructors, and owning pointers (called "unique_ptr"), and also arena allocators for when those are useful. Rust has its Drop trait and analogs of the other things. (Use of arenas with Rust standard library containers is approaching maturity.) They make memory management automatic and wholly painless. Both languages offer reference-counted GC for places where that is helpful, but such places are rare, and where used typically burn an unmeasurably small fraction of runtime, with no "pauses".
> burn an unmeasurably small fraction of runtime
That’s also known as a very short pause.
Your "GC pause" simply happens on scope-exit.
At least bring some alternatives to the table or explain what you are doing and why you mean it's so much better..
For every RAII implementation I've seen, the recursive part is not within the free() implementation; the recursion happens outside the free() (or equivalent) calls, and free() (or equivalent) is called separately for each step, to release the memory for just that step. The time taken by each free() call is independent from the size of the object graph being released.
My understanding is that there are some lock-free algorithms that we do not know how to write without garbage collection [Keir, 2004], so you strictly are giving things up, because you can't use those algorithms.
(But I'm not completely up to date on latest lock-free work.)
And, I don't "do my own memory management". I just don't rely on a GC to do it.
Reference-counting GC is available when it is not too costly, which is common, and more convenient, which is rare. Ordinary automatically-generated destructors handle almost everything, almost all the time. Once in a great while, performance demands a concession such as an arena allocator, which also is not "manual memory management", and also not GC, and is radically cheaper than either one.
I'd use Java if it let me annotate when I want things to be deleted.
It is a fact that the majority of programming in modern non-GC languages, such as C++ and Rust, involves no manual memory management, no GC, and no reference counting (which is also GC). The memory-management automation provided by the core language and the standard library are equal to almost all challenges, all by themselves.
Thus, users of modern non-GC languages do not experience the pauses seen in GC languages, or the unavoidable pointer chasing, or the cache poisoning, or the "reachable leaks" that are GC languages' dirty secret. They are not, in fact, obliged to "trade off" anything at all for being free of those failings.
That is not to say that all design goals are easy to achieve, but overcoming a GC's failings is is not among the activities needed achieve them.
It is extremely rare, nowadays, to "create a destructor" to manage memory. The destructors generated automatically from templates in the Standard Library suffice.
Creating classes is just programming.
So, no, that is not manual memory management.
It's OK that these issues exist - as you say it's just programming - but don't delude yourself into thinking that's automatic memory management. It's implicit, but it's not automatic and that means it's (at least partly) manual. When you say you don't use manual memory management but then say you let standard-library templates handle it, that's a contradiction and tantamount to a lie.
/doubt
Once you're facing, for example, the deallocation of an entire tree that you own and that's going out of scope, your pause is unbounded. Compared to that, a GC'd language could delay the deallocation, or even in case of stuff like C4 have a dedicated thread deal with it.
Sure, you can always write code in a way that avoids this, but non-consing is also an option for many of the GC'd languages (at least the more advanced ones, like Common Lisp).
So, doubt all you like, unavoidable random millisecond pauses are not a problem in non-GC languages, no matter how much you wish otherwise.
You're not using caches, then?
Not using a GC does not, in fact, make more work or more bugs, given a language that provides resource management facilities, such as C++ and Rust.
Programs using GC do typically leak, and most GCs make leaks much harder to find and fix. Most GCs even make it hard to determine if you have a leak.
But that experience does not generalize. Am I spectacularly lucky in my generally competent colleagues? I doubt it.
https://docs.google.com/document/d/e/2PACX-1vRZr-HJcYmf2Y76D...
https://msrc-blog.microsoft.com/2019/07/18/we-need-a-safer-s...
https://support.apple.com/en-us/HT212805
https://support.apple.com/en-us/HT212622
https://support.apple.com/en-us/HT212531
But maybe Apple, Google and Microsoft just aren't able to hire the right kind of highly skilled C++ devs that would write such perfect code, despite having a seat at ISO, and being clang/LLVM contributors.
Perfection is not needed. Ordinary good code suffices. Good code using modern C++, or current Rust, is easier than in older languages (among which count older C++). When bad code is extra work, it becomes an unattractive alternative even to the lazy, leaving its production mainly to the masochistic and the aggressively incompetent, who are often the same.
Here is the thing, when the ISO C++ leaders, and major C++ contributors, push for a change, it is time to start doing some self reflection.