9 ms·
Conservative GC can be faster than precise GC
- notorandit 2y agoJust like a number of (unrelated) algorithms, the sweet spot can be found in the middle between two (or more) optimal solutions. Precision is sometimes useless due to time and computing resources constraints. Speed can come at the cost of poor results. I think the answer depends upon the specific use case and environment.
- tinco 2y agoI've heard about scanning of the stack but I'm not sure if I get it. Is the strategy literally to not keep track of references at all, and simply do a sequential scan over the entire memory looking for bytes that look like they're pointers into the heap, and then assuming they are roots? And then you look them up in the allocation records and mark them as still in use? You'd have to scan them in turn as well to know if they've got references to other heap locations right? Edit: ah he says assume the heap is precisely traced (somehow?) so I guess it would already been known what references are in there.
- Rohansi 2y agoPretty much, yes. There are optimizations done so it would not need to scan every possible location. Pointers should be properly aligned of course but also things like having separate heap regions for data that has no pointers in it (large data arrays) to skip scanning entirely.
- adgjlsfhk1 2y agothe heap is a lot easier to trace than the stack because objects on the heap are put there explicitly, so as long as you know their type, it's pretty easy to know where their pointers are. the tricky part of the stack is that once your compiler has figured out that something can go in the stack, it also might want to do things like store part of the object only in registers or something like that.
- Tarean 2y agoBoth strategies start from roots (e.g. the stack) and then transitively chase pointers. Any memory reachable this way is live. To do this chasing precisely you need some metadata, saying which fields are pointers vs other data like ints. For OOP languages this metadata is stored with the vtables for objects. For the stack you need similar metadata, at the very least how many pointers there are if you put them first in the stackframe. Not having this metadata and conservatively treating random ints as pointers isn't always catastrophic, but it has some drawbacks. Two big ones: - a moving GC is tough, you have to update pointers after moving an object but can't risk updating random ints that happen to have that value - You do more work during GC chasing random non-pointers, and free less memory meaning more GC runs Generating ths precise GC metadata for stackframes is sort-of easy.You need specific Safe-Points where all data is at known locations for the GC to work anyway, usually by spilling resgisters to the stack. These GC checkpoints usually coincide with heap allocation, which is why long non-allocating loops can block stop-the-world GC and send other threads into a spin-lock in many GC implementations Maybe a non-precise GC could treat registers as possible pointers and skip spilling to the stack for Safe-Points? There are alternatives to spilling like a precise stack-map for each program instruction (so every instruction is a safe point), but those are expensive to process. Usually only used for debugging or exception handling, not something frequent like GC
- neonsunset 2y ago> A compiler that does precise root-finding will typically output a side-table indicating which slots in a stack frame hold references to heap objects. These lifetimes aren’t always precise, in the sense that although they precisely enumerate heap references, those heap references might actually not be used in the continuation of the stack frame. When GC occurs, it might mark more objects as live than are actually live, which is the imputed disadvantage of conservative collectors. This is not necessarily accurate with true precise tracking E.g.: using System.Runtime.CompilerServices; Example(); // Skip Tier 0 compilation which does not track gcrefs as precisely [MethodImpl(MethodImplOptions.AggressiveOptimization)] static void Example() { var obj = new object(); var wr = new WeakReference(obj); for (var i = 0; i < 3; i++) { Console.WriteLine(obj); } GC.Collect(); Console.WriteLine(wr.IsAlive); } This works in much more advanced scenarios too. Unfortunately, I can't link a simple document that covers this in detail from the top of my head but there's a wealth of information in Konrad Kokosa's works: .NET GC internals lectures: https://www.youtube.com/watch?v=8i1Nv7wGsjk&list=PLpUkQYy-K8Y-wYcDgDXKhfs6OT8fFQtVm https://www.youtube.com/watch?v=8i1Nv7wGsjk&list=PLpUkQYy-K8... Pro .NET Memory Management (which is a great book in general): https://prodotnetmemory.com/ https://prodotnetmemory.com/
- andyayers 2y agoIn .NET, even in optimized methods, there can be "untracked" lifetimes where a stack slot is reported live to GC throughout the extent of a method, so presumably these can lead to the "over-reporting" cases mentioned. The number of trackable lifetimes was 64 in .NET Framework but has been steadily increased in modern .NET and is now 1024, so it's rarely a capacity issue; but there are cases where we can't effectively reason about lifetimes. For us another big drawback to conservative scanning is that any object referred to by a conservative reference cannot be relocated, since the reference might be live and is not guaranteed to be a GC reference; these objects are (in our parlance) effectively pinned, and this causes additional overhead.
- neonsunset 2y agoThanks! I knew about 1000 (turns out 1024) limit for method locals, in hindsight it does make sense for it to apply to gcref tracking just as much...
- DarkNova6 2y agoI'm not sure what this article is trying to convey to me. I can only suppose the intended audience is entirely academia-centric. Reading the conclusion, I don't know if anybody working on actual top-tier GCs (Java, JavaScript, C#) finds this useful. It strikes me more as a somewhat interesting factoid, as demonstrated by a paper. The conclusion: > When it comes to designing a system with GC, don’t count out conservative stack scanning; the tradeoffs don’t obviously go one way or the other, and conservative scanning might be the right engineering choice for your system. If there would be examples of how this relates to actual GCs in production and compares them, now that would be interesting.
- pizlonator 2y agoMost production GCs are accurate and the article’s position on conservative GC is a minority position.
- mseepgood 2y agos/accurate/precise/
- pizlonator 2y agoNo. The literature uses “Accurate GC”, “Precise GC”, and “Exact GC” interchangeably. Famous paper on this that uses “accurate”: https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=db35502708be06db3bfdd26c685400f3e134fdd4 https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&d... Paper that calls it “exact”: https://dl.acm.org/doi/10.1145/2660193.2660198 https://dl.acm.org/doi/10.1145/2660193.2660198
- samatman 2y agoAs the Fine Article mentions, the JavaScriptCore GC is conservative, and V8 is considering a switch. Someone reading your sentence might be at risk of conflating a minority position with a fringe one. Clearly this isn't the case here.
- 2y ago
- nu11ptr 2y agoI've never understood why anyone would use a conservative collector outside toy programs or academia. It is hard enough to make programs deterministic even with precise collection. I can't even imagine releasing software that was inherently non-deterministic and could suddenly, and without notice, start retaining memory (even if atypical in practice). Thus, IMHO, which is faster is a moot point.
- sestep 2y agoYeah... I can't imagine trying to debug that. "We kept getting memory leaks, so we dug into it and realized that the language was interpreting local integer variables as pointers and refusing to free memory, but this only happened sporadically and we couldn't reproduce the bug on our development machines. After banging our heads against the wall for weeks we realized what was going on, and it turns out this behavior is completely intentional and they have no plans to change it."
- hypertele-Xii 2y agoComputer programming is full of probabilistic edge cases with ridiculous costs that are amortized over normal use. Most optimization, encoding, and compression is based on statistics. If your use case requires a minimum bound, use another algorithm.
- sestep 2y agoAmortized analysis actually provides a guarantee (either deterministic or probabilistic) that things will tend to even out in the long run. Unless I misunderstand something, conservative GC provides no such guarantee, and there are no hard statistics behind the claim that memory leaks caused by it should be rare. There's a difference between "this algorithm is actually random and so sometimes will happen to exhibit suboptimal behavior" and "under the right circumstances, this garbage collection scheme will consistently produce memory leaks due to arcane rules, but those conditions are practically impossible to reproduce in a controlled setting."
- chrsig 2y agoIt was a bit of a bummer when go switched from the conservative gc to a precise gc. One of the implications was that they needed to change how interface types were represented. They had a nice little optimization for word-sized values to store in-place rather than as a pointer out to a value. With the precise gc, they had to make the change to only storing pointers, leading to allocating small values. I don't know if they've done work to (or perhaps better put: had success) regain the performance hit from the extra allocation & gc load. On the flip side, my experience is that they've made the pretty unobtrusive with regards to latency and pauses. Or perhaps I'm just not stressing it as much as I had in the past. Random anecdote on gc tuning: I was once dealing with a go process that sped up (higher throughput, lower latency) by an alarming amount limiting the max processors. This was many years ago, and I wouldn't be able to say what version of go. It was after you no longer had to set GOMAXPROCS, but probably not very long after. Performance tuning is crazy sometimes.
- znpy 2y agoPerformance tuning is still largely a dark art from what I see, having dabbled in the space. It’s both weird and beautiful because getting an end-to-end understanding is instrumental, so you often have to go look at many marginal things that might actually play a significant role.
- bqmjjx0kac 2y agoDisappointingly, it's a dark art often because the CPU is a black box. Intel X86 chips translate the instructions you give them to some internal microcode and then execute them speculatively, out of order, etc. I'm still mystified by the performance gains afforded by randomly inserting NOP instructions.
- pjmlp 2y agoThat is why tooling like VTune exist.
- deleted 2y ago[deleted]
- cancerhacker 2y ago“One day a student came to Moon and said, I understand how to make a better garbage collector. We must keep a reference count of the pointers to each cons. Moon patiently told the student the following story: One day a student came to Moon and said, I understand how to make a better garbage collector...”
- deleted 2y ago[deleted]
- gok 2y agoI would have assume the major benefit to precision is that it enables compaction…
- sfink 2y agoYou can still compact with a conservative scanner, you just have to accommodate pinned regions.
- themk 2y agoHow do you compact with conservative GC? You can't change the pointer values because they might not be pointers right?
- dzaima 2y agoAny object which is referenced by the stack cannot be moved, but the rest of the heap (i.e. the vast majority, assuming the stack is much smaller than the heap) still can.
- kazinator 2y agoConservative GC is quite vicious against the use of lazy lists: You have code like this: function(make_infinite_lazy_list()); where function walks down the list: fun function(list) { while (list) { // nil is false ... list = cdr(list); } } problem is, the parent stack frame contains a spurious copy of the original return value from make_infinite_lazy_list, and so as function marches down the list, the discard prefix of the list is not becoming garbage, as expected. This is the showstopper for conservative GC. Not the bogeyman of a stray machine integer suddenly looking exactly like a heap pointer. Stick a conservative scanner under C, make yourself some lazy lists, and this problem will easily reproduce!
- celeritascelery 2y agoWouldn’t a stack map have that value as well?
- kazinator 2y agoIf the compiler puts out a stack map that is conservative, then the GC scan will be effectively conservative. The compiler has to compensate for that somehow. If a temporary location is in the stackmap, such that the value in it is a dead value before a function call, the compiler has to insert an instruction to null out that temporary location.
- rurban 2y agoOf course it can be faster because it's wont need a shift, ptrs not a mask and objects not 2 words. But usually you want to free ints which look like ptrs earlier, sync don't keep them around. It's rare, bug when happening a memory hog. Esp. On long running processes I would only do precise GC. Those simple bit shifts and masks cost almost nothing today. For C interop you need to track pointers anyway separately.
- sfink 2y agoI find this to be more of an interesting observation than a reason to choose a conservative scanner. I've never really thought of conservative GCs as faster or slower. The stack scan is fast, simple, and prefetchable, but occasionally has to do extra work. The precise scan has to consult separate tables, often not stored nearby. I guess I'd expect the speed difference to come more as a result of conservative pointers getting pinned and what repercussions that has on the overall collection. I did think it was interesting that precise scanning can sometimes be more conservative than a conservative scanner because of excessively long live ranges. It's great to have a wingo writeup on these things. Precision gives you a lot more flexibility in moving data. Conservative scanning is way more pleasant for embedding (you don't have to go through a ritual to register every root). The difference in what gets collected rarely seems to matter much in practice, though the determinism can matter, especially for intermittently failing tests. There's a lot of path dependence in choosing one over the other. It's easy to go from precise to conservative, but very hard to go the other way. In practice, that means you tend to stick to whatever you've got—if you have a working precise collector, you're loathe to loosen it up because you might get stuck. I was heavily involved in moving the SpiderMonkey collector from conservative to precise, and I wouldn't like to lose that work, and I'd really hate to have to do it again! And yet, the maintenance cost of keeping the precise collector correct is not low. I suspect a lot of the argument for precision boils down to: bump allocators go brrr. With a precise scanner, you can bump allocate into a young generation, move everything out during a minor GC without regard for conservative pinning, and then do it over again. No looking at allocation bitmaps or free lists or whatever. Easily JITted (at least for the basic allocation), good locality, simple code. A more tenuous guess is that a lot of the argument for being conservative boils down to ease of embedding. You mostly don't have to think about registering your stack roots, you can just allocate stuff and call things.
- tmendez 2y agoI'd be curious to run experiments on this with one of my Go services. However, as far as I can tell, Go's garbage collection is precise and there's no way to run a Go program with a different (conservative) garbage collector. Am I overlooking something or am I out of luck here?