5 ms·
What garbage collection actually costs
- miladyincontrol 2mo agoWhat does GC cost? For Caddy with an incredibly synthetic http only benchmark it costs about 2ms of latency and somewhat less throughput. Worth it in an incredibly artificial benchmark? Perhaps. However when it comes to real world usage the cost is a significantly smaller piece of the pie.
- shivanshuag 2mo agoAgreed, for most real world softwares, the cost of GC is irrelevant. But there are still some programs like databases or game engines where the cost can start adding up. That's when you measure and optimize.
- pjmlp 2mo agoYet the three major game engines Unreal, Unity and Godot all have a GC on their infrastructure, and Capcom is quite happy with their .NET fork on RE Engine. Also every single graphics application that uses Metal or DirectX, relies on reference counting as GC algorithm.
- slopinthebag 2mo agoI’m sure those three engines have had no issues with performance whatsoever right? Oh shit…
- izacus 2mo agoDo you have any source taking about GC caused performance issues in those engines?
- slopinthebag 2mo agoToo many to post, you can just google “{engine} gc spike” for example. Heck, there is a whole cottage profession of experts who get called into fix GC related performance issues with Unity.
- jayd16 2mo agoSo are you saying this cottage industry achieves success or the industry formed around an impossible task? A cottage industry is built around making rocks look good too. Is that an indicator that games are good or bad at making rocks?
- izacus 2mo agoI work in performance optimization (I guess the cottage industry you're talking about?) and GC issues haven't been close to top 10 problems in any of the engines or OSes we've dealt with lately, so I'm wondering what you're on about there. Can you quantify and elaborate more how much GC is an issue for something like Unreal engine?
- pjmlp 2mo agoI am sure that many of the issues were a skills issue as well.
- jmull 2mo agoGame developers are always trying to push the boundaries. The only game engines without performance issues are ones hardly being used.
- slopinthebag 2mo agoThere is a difference between pushing more polygons or higher resolution, and trying to fix stutters caused by the GC…
- pjmlp 2mo agoAlternatively they could be fixing stutters caused by using malloc() on the wrong place.
- slopinthebag 2mo agoMuch rarer and easier problem to deal with. Besides, bump allocators are widely used in games programming, which essentially eliminates this entire class of performance issues. Good luck using one in a GC'ed language.
- jmull 2mo agoThere isn’t though. Garbage collectors aren’t magical black boxes with no levers, and you can use them how you like. You address performance problems relating to GC the same as you do anything else: measure, analyze, optimize, repeat.
- slopinthebag 2mo agoThe difference is that you don't need to be pushing the boundaries at all to get into trouble with the GC. All you have to do is use it as it's intended.
- deleted 2mo ago[deleted]
- jayd16 2mo agoWhat engine do you recommend?
- slopinthebag 2mo agoDepends on the game
- dismalaf 2mo agoI mean, you can always add enough triangles, shaders, entities, etc... to make any engine slow to a crawl... Anything that pushes state of the art will have more performance issues than something that doesn't.
- marcosdumay 2mo ago> That's when you measure and optimize. How do you "optimize" the GC away after you wrote your entire database server in a language that uses it?
- ApolloFortyNine 2mo agoIt's incredibly in common in game development, C# has a lot of features you can take advantage for this. But the most naive example any language supports is simple object pooling. Then more fancy, zero allocations tasks in C# https://github.com/cysharp/unitask https://github.com/cysharp/unitask
- jmull 2mo agoGC languages typically have features of the language and/or standard library that make GC the default, not the only option.
- marcosdumay 2mo agoWhat doesn't save you from having to rewrite the entire system. (Even though, no, that's not typical. That's a tiny minority of them.)
- jmull 2mo agoNot sure I understand the question, but it generally works like this: Once you measure, you'll find a small fraction of the code is taking a large fraction of the time. When you zoom in on trouble-spots you may find, e.g. that the GC is taking the time (or you may find something else entirely is taking the time). If it's the GC, you might look and see, e.g., that it's spending its time tracing the objects in the 100K node graph you're creating several times a second, and realize you could, e.g., create it once and simply keep reusing it. Perhaps it might be as simple as using removeAll(keepingCapacity: true) instead of removeAll() (a Swift example). The superficial details differ, but you generally just want to understand what the GC is working so hard on and lighten its load. If you haven't been measuring and optimizing throughout, there are almost certainly easy-to-pluck, low-hanging fruits, ripe for the taking.
- slopinthebag 2mo agoThis assumes that you’re only ever running a single software at a time. Sure, 2-10x slower/memory consumption might not matter in a vacuum, but when every software is like this, you get machines that feel slower than they did 2 decades ago.
- jayd16 2mo agoDid you "Agreed" your own blog?
- Sha1rholder 2mo agoThis is getting ridiculous.
- pclowes 2mo agoThis is one of the best high-level survey explanations of GC I have seen, nice work.
- 220hertz 2mo agoI used to write a lot of Javascript-like Extendscript scripts back when I was using InDesign a lot. The DOM's global object $ had a method to directly invoke the garbage collector. It made a difference certainly, but it was difficult to tell to what extent because InDesign itself gradually leaks memory and becomes more bloated the longer you use it in a single session.
- thomashabets2 2mo ago> In Rust you pay for it by arranging your program in a way the compiler can verify. I disagree with this. The sentence implies that this work is done in order to make the compiler happy, where my experience is that it forces the programmer to actually get it right. I had an "aha moment" when I was frustrated at failing to express my intent to the compiler, and suddenly realised that the reason I couldn't "just say the magic words" was that my object ownership design was inherently flawed. I had to make large changes not to make the compiler happy, but to actually have a coherent design. So no, it's not about what "the compiler can verify". That's like saying "my lawyer won't let me do this". No, your lawyer is your employee, not your boss. They're just saying that if you do this, then you may go to prison. It's not the same thing. ("unsafe" is the Rust way to go "thank you, legal department, but I'm making a business decision to take this risk. Your concern has been noted")
- ron_k 2mo agoI understand what you’re saying, but I read that phrase in a different way. Let’s say you have two ways of doing the same thing: both work, both are legit and neither introduce GC bugs. The only difference between the two is that one can be verified by the compiler while the other can’t, so you are stuck with solution no. 1 although both would work. To phrase it differently: the code that gets verified by the compiler is safe, but is all safe code verifiable by the compiler? I’m not implying that’s the case, but that’s what I feel the author is saying.
- deleted 2mo ago[deleted]
- thomashabets2 2mo ago> To phrase it differently: the code that gets verified by the compiler is safe, but is all safe code verifiable by the compiler? Right. And this reduces to the halting problem, so in theory the compiler cannot know that all safe code is safe. In practice, I'm saying that not just syntactically, but in your code's design, the compiler is more likely to be right. It's a bit like Chesterton's fence. You can bypass the lifetime checks if you just have the confidence to say "yes, I'll use `unsafe` here and it's fine because these reasons". As you're writing your "SAFETY" comment, you may very well find yourself not so confident anymore. And indeed, often this compiler-induced "stop and think" prevented you steaming ahead with a bug. Now, the borrow checker is not perfect. I don't know how far away from "all but NP-complete cases" it is. My experience is that it's almost always right, and I've only had to put a seemingly needless "drop" statement to placate it. But they're working on it. A new one is coming: https://daily.dev/posts/rust-s-new-borrow-checker-is-coming--llpxltaj6 https://daily.dev/posts/rust-s-new-borrow-checker-is-coming-... And once again this old blog post of mine comes to mind: https://blog.habets.se/2020/12/Bypassing-safety-check-for-obviously-safe-changes.html https://blog.habets.se/2020/12/Bypassing-safety-check-for-ob... In any case "by arranging your program in a way the compiler can verify" I think is not accurate, because the overlap between "correct" and "compiler can verify" is nearly complete, though yes the latter is a strict subset of the former. In other words I don't write Rust to make the compiler be able to verify it, but to make it correct. And nearly always that means the compiler can verify it too.
- EGreg 2mo agoThere is no need for garbage collection if you don’t form circular references. Just have a canonical direction and always keep weak references the other way.
- thomashabets2 2mo agoReference counting is a different model. Many papers have explored the differences and similarities, and your comment leaves so much out that it cannot even be said to be true or false. I'd say GC is always the fastest to free objects within the main code path. Literally zero instructions.
- EGreg 2mo agoIt doesn't leave anything relevant out. Java pioneered this garbage collection stuff because you had cycles of references. You don't need to have cycles. WeakRef is a much better thing now. All you need is reference counting, and you don't need any garbage collection at all. When the reference count reaches 0, you destroy the object and free up its memory. It's far more predictable than GC, too. And GC isn't "the fastest" to free objects, it has to walk a graph. The fastest is actually arena allocation and then just dropping the whole thing. But that's exactly what owning an entire container of objects can do. If you have a doubly linked list, for example, A[n] -> A[n+1] but also A[n+1] -> A[n] but neither of those should be a strong reference to prevent reclaiming. Instead, the container of that doubly linked list should be the one having a strong reference to its items.
- thomashabets2 2mo agoI understood your first comment without expanding on it like this. You also don't need to explain what reference counting is. > GC isn't "the fastest" to free objects, it has to walk a graph. No, you don't inherently need to. And what I said is that it's super duper fast to produce garbage. I did not say that actually freeing the underlying memory, or any other cleanup, was fast. I'm not even arguing for GCs, here. I even think GC languages tend to become tech debt generators, as garbage is not addressed until it's a really complex problem with no good solutions.
- amazingamazing 2mo agoI rarely see a real use case bottle necked on garbage collection.
- bjourne 2mo agoProps for using a correct nomenclature. Reference counting is a "kind of automatic garbage collection. Tracing garbage collection is also a kind of automatic garbage collection.
- kazinator 2mo ago> Think of a service that keeps a large cache in memory, or an index built out of millions of small objects that all point at each other. Every one of those pointers has to be followed on every cycle, for as long as the process is up. That's a strange thing to assert, having acknowledged the existence of generational GC.
- Someone 2mo agoSimilarly, the statement > Reference counting also has its own running cost, paid on every copy of a pointer you make and every time you drop one. isn’t 100% true. It’s not necessarily on every copy or drop. Compilers can (and do) elide reference count updates if they can proof they aren’t needed, and can even skip allocating room for reference counts if they can proof it isn’t needed (example: a local object that doesn’t escape its scope) I also find it a miss that the article doesn’t discuss memory usage. A garbage-collected program needs more memory to match the performance of the equivalent manually managed language. https://dl.acm.org/doi/10.1145/1094811.1094836 https://dl.acm.org/doi/10.1145/1094811.1094836 says you need to give it 5 times the memory, but that’s from 2005 and likely outdated.
- kazinator 2mo agoIn 1990s C++ we did: void foo::method(const &smart_ptr<thing> x) { ... } The smart pointer passed reference isn't copy-constructed and so no refcount is bumped. It has a scoped lifetime. The calling function owns a reference (baseline correctness assumption or else we are screwed). That caller is suspended while the callee executes.
- deathanatos 2mo agoIt's still done. It's not uncommon to see that in Rust: fn foo(x: &Arc<T>) …quite literally borrows the count in the refcount-ed thing. I think you can find similar things in CPython, on the C side of things.
- pjmlp 2mo agoOnly if reference counting is exposed in the type system, or the standard library types are blessed to the compiler, otherwise there is no way for the compiler to know what to elide.
- kev009 2mo agoEven things built directly on underlying malloc and free typically have some form of "garbage collection" in the malloc implementation for efficiency and performance (geometric sizing, thread caching, etc). It's best to think about lifetimes and lifecycles where possible. Immutability where sensible and things like pool allocation are examples of this. GC languages can result in quite pessimistic code because they encourage people to NOT think about what is going on. But people have also built functional HFT engines on things like the JVM by thinking about lifetimes and lifecycles.
- jerf 2mo agoUltimately, the entire problem of deallocation in general is a continuum, not a binary, and I tend to find it hard to take anyone seriously who is vigorously arguing about how awful GC is if they don't understand that and indicate some understanding of the concept. The closest you can get to real "manual" memory management on a modern system is to use nothing but arena allocations, and if you really want "manual" memory management, you need to allocate some small fixed number of arenas, because if you're constantly allocating and deallocating them that is itself probably an automated process that could go wrong, at least in theory. Malloc/free or new/delete isn't really "manual memory management". The wide variety of options and tradeoffs, with fewer clear lines in the sand than most people seem to think, is already enough to call it a "continuum" but what really finishes the job is that they're all mixable and matchable. Something like Zig makes that really obvious, but most static languages have at least some sort of ability to mix in multiple strategies. There's nothing wrong with a C++ program that uses new & delete, and also uses arenas for some things, and also uses garbage collection for some things, and also has an integrated scripting language like Lua with its own memory strategies. Such programs are not that uncommon... that describes modern games nowadays, the supposed canonical case where you "can't afford GC". But it can... it just fences it in to a particular domain where it fits.
- netbioserror 2mo agoThere's a third way here. It could be called many things: Single ownership by default, automatic stack lifetimes, hidden unique pointers, etc. The main idea is that the lifetime of dynamic heap data is treated no differently to primitive stack data: Clean it when it goes out of scope. Rust and C++ require you to specify this manually, but Nim is unique among native-compiled languages in that is does it by default, with tools to opt-out. An advantage of this approach is that combining immutable values and static analysis can reduce most parameter passing to borrows, again, without needing the programmer to specify, by default. The main cost being that some assignments, especially crossing the variable-to-immutable line or vice-versa, would require a copy.
- melodyogonna 2mo agoNot really the point of the post, but there is a third memory allocation paradigm where you can do manual allocations and deallocations, but without the mental overhead of having to remember to free memory because the compiler will force you to properly handle it. Mojo has linear type support so you can do something like this: https://x.com/melodyogonna/status/2085089269484343725?s=20 https://x.com/melodyogonna/status/2085089269484343725?s=20
- gwbas1c 2mo agoCareful, this statement is misleading: > The leaks that come from forgetting to free something go away entirely. Not quite: In manually-managed and referenced counted languages with destructors, releasing resources, (open file handle, open socket, open connection to a database, ect,) happens when objects are cleaned up. In a (tracing) garbage collected language, releasing resources is a very manual process. You might not have a memory leak, but leaking file handles or similar resources is a real problem with real consequence.
- TrustScoreAgent 2mo ago[flagged]