Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Gossamer has a cycle collector and eager reference counting. Good luck dropping the last reference to a 10,000-node graph, especially if cyclic. That means it doesn't have "pause free" memory. If you want pause freedom, go use ZGC or another modern GC on a modern VM.

I just can't take seriously this spate of languages that ignore the past 30 years of research into automatic memory management. We have multiple open-source pauseless miracles GCs right there before our eyes, yet it's the trendy thing in language design to foist memory management on users.

You don't even have to use a big VM if you want good GC. Go use MPS. Lots of options out there, even if you want to implement your own VM.



> We have multiple open-source pauseleses miracles right there before our eyes

Is this meaningfully true in a practical sense? I've been writing code with soft real-time requirements and I don't think your notion of "pauseless" suffices. And if these miracles are open-source and right before our eyes, why do languages like Crystal and D still use Boehm?



> why do languages like Crystal and D still use Boehm?

Languages use Boehm for exactly one reason: it is easy to shim into an otherwise manual memory system (it was designed for use in C/C++). I mean no respect to its authors, but using Boehm in production is the worst of all worlds: slow allocations (free list allocator), poor cache locality, and not precise (so you can expect memory leaks). If you are going to do a GC language you want: 1) precise 2) bump allocator 3) compacting collector 4) generations. Essentially you want to allocate fast, only touch live objects (most objects die young), compact them for locality, and only process objects each cycle of similar age. There is a huge amount of engineering that goes into a state of the art collector, but those are the basics.


This is all true but is a somewhat Java-flavoured perspective i.e. generations ties you into a moving collector, which ties you into barriers and complicates FFI, which is not always the right tradeoff.

A non-fragmenting allocator goes a long way to alleviating the need for compactions too.


Not necessarily Java-flavored, but internally vs externally focused, yes. More difficult FFI assuming that is the exception not the rule, and that the language itself takes precedence. Write barriers are also not a given if using segmented heaps. Many ways to do this and no single right way. Memory allocation scheme isn't something that is just bolted on, but needs to be aligned to the rest of the language. For example, Java needs such fast allocations and good GC because it does almost no inline allocation whatsoever, so without the best GC on the planet, it would be a lot slower than it is. Contrast this with Go, which has a solid amount of inline allocations, and hence, can get by with a much slower allocator (~3-4x slower by my measurements) and a more basic mark-sweep allocator since the memory pressure is solidly less.


The charitable explanation is the authors lack the time to rebase onto something more modern.


You seem to have a very low opinion of other people. If these miraculous collectors are so generally applicable, why are very smart people putting effort into things like Perseus?


Smart, honest people can have sincere and earnest disagreements. I believe the manual-memory-management people are mistaken. That's not to say they're stupid: it means I believe they're going down the wrong path, as smart people have done since time immemorial. I wish them all the best. That said, I must wonder what other innovations they reject if they insist that GC is unacceptable.


We insist that GC is unacceptable only because we insist that uncontrollable latency is unacceptable.


And we disagree on how best to gain control of latency. Some say that the way to gain control of memory management latency is to track object-level allocations locally using malloc/free-style APIs, arenas, and so on. IMHO, low tail latency achieved through this approach is fragile and often illusory: object reference graphs are often bigger than you expect, and malloc/free-style heap managers (even with thread caches) need to do global synchronization eventually. Arenas work for some cases, but often break down for complex programs. (Look at libapr!)

No, I think GC is the way to control latency of memory handling. Plenty of work on real-time GC shows that you can construct a GC such that if the mutator allocates less than X MB/second you can achieve reclaim latencies under Y ms. The nice thing about these guarantees is that they're global: it doesn't matter how you allocate. All that matters is how much you allocate. It's a metric you can measure and optimize, IMHO, more easily than you can try to bound heap-manager contention and free-SCC size.

Granted, you can come back and point out that it's hard for me to prove I don't have allocation-rate spikes just like it's hard for you to prove you don't have lumpy free()s and malloc pool contention. But IME, it's a lot easier to bound latency rates, because we have good allocation profilers and in many cases you can prove allocation caps. IMHO, it's much harder to reason about long-range interactions of threads touching heaps.

The one primitive I wish we had but (outside BEAM) don't is object coloring. In a GC system, I should be able to allocate objects from different heaps and GC them independently .This way, a can write a subroutine that I can prove locally obeys the allocation-rate rule for my latency target and doesn't do any global allocations without giving up the use of the global heap for other purposes. They'd be a bit like the explicit arenas your tribe uses. (Your child heap would count as one "object" for purposes of parent-heap retention.) You could probably adapt existing multi-pool systems like MPS and ART's GC pretty easily too.

But even absent QoL features like these, modern GC is plenty suitable for programs that need to be responsive.


Thank you for the insightful comment.

I feel I have to insist that to control latency one has to first eliminate any intrinsic source of it (a GC) and only then deal with what's left (using arena/pool allocators and such) in the face of even soft realtime requirement (which imo should be the default unless utterly impossible).

This is comparable to what you say about coloring, like, if say java had _both_ GC and zig-like allocator api, so one could have fine control over what is happening. Unfortunately it does not, and the idea of not putting all eggs in one basket is somehow unthinkable.


The entire concept of a pauseless GC is that you have no uncontrollable latency. The GC can run on a background thread with zero stop-the-world.

Of course, this assumes you're in a preemptive environment with access to other threads, etc.


@LoganDark You're right, as long as there is an object graph to scan, 'uncontrollable' latency is an inherent trade-off in GC-based systems. I’ve taken a different route with a C++20 execution engine that eliminates the object graph scan entirely by using pre-allocated, static memory pools and lock-free SPSC structures. It's essentially moving from 'managing GC pauses' to 'deterministic, zero-allocation execution'. Have you ever benchmarked your systems against a lock-free architecture that bypasses the allocator on the hot path?


The lock-free architecture that bypasses the allocator on the hot path is called `alloca`. Many mainstream compilers, and nearly every language that is not C, seemingly haven't properly supported it for years.


Please don't twist. alloca is in fact a limited arena/pool allocator and as such for some small sizes it is latency-free. It is also there in C. It does not have anything to do with GCs.

You insist that offloading other allocations to a GC running on other threads somehow grants the current thread control over its own scheduling.

This is false.

One can say that you can't really control latency unless running under a RTOS and thus we shall never attempt that. This is also false.

There is no conceivable reason one should not assert absolute control upon what his own code does. GC injects uncertainity into that. This is intolerable.


> We have multiple open-source pauseless miracles GCs right there in front of us

Can you share some links/references?


ZGC is extremely good work.

https://wiki.openjdk.org/spaces/zgc/pages/34668579/Main

> ZGC performs all expensive work concurrently, without stopping the execution of application threads for more than a millisecond. It is suitable for applications which require low latency. Pause times are independent of the heap size that is being used. ZGC works well with heap sizes from a few hundred megabytes to 16TB.

Go's GC is also very good: https://go.dev/blog/greenteagc.

V8's Orinoco is also pretty good now. It's improved a lot over the past decade and is now mostly-parallel. (A decade is about how long one of these things takes: high-performance GC is hard.)

I'm also a fan of MPS: it's a big of dark horse because it's more a GC construction kit than a ready-to-go GC, but it's fast and flexible, and I'd start with it any day over Boehm if I were making a VM from scratch.


If I were writing this language, I'd probably just compile it to Go, although that means Rust extensions would either incur cgo costs or have to be replaced with Go extensions.


Cgo is cheap these days, don't worry about it.

It's barely more expensive than a direct function call but, not so you'd notice unless it's in a hot loop. At which point the lack of cross-language inlining is your real problem.


Do you know of any articles, tests, or implementation breakdowns that show this. I don’t have the personal experience to agree, but if that’s the case Inwould really like to know how the improvements were achieved.


It was somewhat slow about a decade ago - you can see

- (2015, Go 1.5) Calls cost about 170ns https://www.cockroachlabs.com/blog/the-cost-and-complexity-o...

- (2017, Go 1.8) Cgo speedup by 50% https://go.dev/doc/go1.8#cgoperf

- (2023, Go 1.21) Calls cost about 40ns https://shane.ai/posts/cgo-performance-in-go1.21/

- (2026, Go 1.26) Cgo speedup by another 30% https://go.dev/doc/go1.26#faster-cgo-calls

A current benchmark shows a Cgo function call as costing about 25 ns. https://gist.github.com/DeedleFake/2f50b02c0708484c66d182533...


You just described http://www.lisette.run


A millisecond is an eternity. It is 1/3 of the entire time allocated to a frame update in a modern game.


Various GCs can go faster now too. JEP 376 talks about hundreds-of-microsecond work done in pause now that GC no longer has to scan the whole stack.

That said: 1ms? 1ms is getting into the sorts of latency the OS and hardware impose on your program no matter what it does. For example, on x86, a SMI can take 300us, or 1000us if you're unlucky. I've seen softirqs for shitty wifi chips take a hundred milliseconds! And God help you if you take a hard page fault:

You're worried about 1ms latencies, right? So you're mlock()ing all memory? Running RT threads pinned to cores? Carefully using PI and static priorities to avoid inversions? Avoiding blocking IO everywhere, not even for graphics page-flipping? Managing thermal headroom to avoid involuntary clock collapses? And it should go without saying, but I have to ask: you're running a PREEMPT_RT kernel, right?

No? You're not doing any of these things? Then why are you worried about 1ms in GC?


Yes, I am. That’s why I develop on a systems programming language, and why systems programming and GC are not compatible.


I think this is a situation where the term systems programming is too un- or ill-defined to be anything but a semantic argument in waiting. I am not particularly fond of the broader meaning systems programming has taken on but I understand it. As a term of art for developers systems programming now encompasses:

   - infrastructure development like Docker and Kubernetes
   - general utilities programming like grep, terminal emulators, compilers, etc
   - performance sensitive artifacts like OS kernels, video/audio codecs, hardware interaction layers
and more in common usage. Without some sort of communal understanding of the taxonomy of development areas discussing things like GC in systems programming becomes tedious and often prone to arguing past people due to conflicting understanding of terminology.

I do think there are genuinely ripe areas of research and development for performance and determinism sensitive memory management and subsequent outreach to make sure the potentially effected developers and language designers actually have a chance to evaluate any advancements. But it sure seems like it would take an act of ‘developer congress’ to make sure people were talking about the same things.


What are you describing as a frame update? I am trying to be as generous as possible to your comment and thereby assuming there is a disconnect between my understanding of ‘frame update’ and yours. Genuinely curious, because that term as I am familiar with it means the entire time allotted to generating and rendering a single frame for display. If that’s what you meant, you are asserting that a ‘modern game’ is typically running at 333 fps. I am typically using the term ‘modern game’ to describe newly releasing AAA games nearly all of which struggle on many hardware configurations to achieve a steady 60 fps (which is 16 ms per frame).


e-sport competitive games target either 128Hz or 320Hz update frequency. Two rules of thumb: first if you want to actually get 60Hz every frame (e.g. on a console where 1st party validation cares about this more), then you attempt 120Hz so jitter only brings you down to 60Hz. Second, a bit more than half of your time is spent doing other things. So the amount of time you actually have to spend on world-update and/or render logic is only about half of your allocated time to start with. That's two independent factors of two there. I would have targeted 90Hz to start with, which gets you to about 3ms of actual time per frame.


Game GCs are interesting because you know that the execution is structured like this and you know how much time you have left before you have to switch back to application code for the next frame/time step. There's interesting optimizations you can make around this and could almost completely avoid user-observable GC pauses.


Exactly. This is why Minecraft Java edition was such a disastrous flop.


They're talking about modern games running on screens with high refresh rates.


Are any of those actually pauseless like he asked for?



The one plus I'll give reference counting is it still takes the cake for interoperability with C. Which is only important if you need good interoperability, but when you do, tracing GCs don't play nice.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: