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

The key mechanism for memory safety, generational references, is described here: https://verdagon.dev/blog/generational-references

IIUC it's basically a form of memory tagging in userspace, but designed to be safe (64 bits, very low risk of collision) and fast (language is designed to avoid tagging whenever possible: owning refs don't need them; inline objects reuse the gen of the parent; static analysis).

I buy that this can be almost as safe as reference counting or GC, and as easy to use, but faster. But not sure I understand how inline objects work, and how effective static analysis will be is important.

Comparisons to other languages (C++/Rust/JS) are here: https://vale.dev/comparisons



Good summary!

Generational references were less than half the overhead of RC as of last benchmark. [0]

Re inline data: A piece of inline data will (most often) share the generation of its containing object. A generational reference can include an offset to the parent's generation, which we can use for generation checks.

Inline data should make it even faster, because then we can control our objects' layouts and use the cache more efficiently, and not be chasing pointers all the time. I think that's the biggest advantage of generational references over RC and GC.

We're also building regions into the type system, which can be used to statically temporarily freeze areas of memory (similar to the Rust borrow checker) to eliminate generation checks. [1]

Another very experimental aspect we're prototyping is "hybrid generational memory" which can temporarily lock an object (similar to a RefCell) to elide a lot more generation checks [2] but it's too early to promise it will work.

[0] https://verdagon.dev/blog/generational-references

[1] https://verdagon.dev/blog/zero-cost-refs-regions

[2] https://verdagon.dev/blog/hybrid-generational-memory


But

1. This technique prevents undefined behaviour, by halting whenever an invalid dereference is detected. While Rc is a garbage collection algorithm. So you are comparing apples and oranges, no?

2. Rc is a bad gc algorithm. How does this compare to a good quality, generational, incremental gc?


> A generational reference can include an offset to the parent's generation

I see, thanks for the clarification. Then interior pointers will be larger than normal pointers, like all generational pointers? While the interior object itself doesn't have a generation, avoiding that overhead. Makes sense I think.

All the larger pointers do make me worry about increased memory overhead, though (kind of the reverse of the x32 ABI which has half-sized pointers; I think 5-8% is the quoted perf difference there). Do you have benchmarks of memory overhead - looks like the link has throughput?

Regardless, it sounds like the other features you mention should help with both forms of overhead, so it will be interesting to see how much.

Cool project btw!


Thanks!

The increased memory overhead would seem to be a problem, but in the programs I work on at least (games, web servers, compilers), the vast majority of references in a program are owning references which aren't fat pointers. Non-owning references are very short-lived and on the stack, so the memory overhead shouldn be pretty minor in practice.

Also, if we want to save a little more space, we can reduce the generations to 32 bits (we're leaning this way in fact, as it opens the door to some interesting other features).


> To dereference a generational reference, we do a "liveness check" to see whether the allocation's generation number still matches our reference's target generation.

How does this work with threading (use after check)? Or are allocations always limited to a single thread?

> This will safely halt the program

So it’s not statically memory-safe. That’s not hugely attractive I have to say.


> So it’s not statically memory-safe. That’s not hugely attractive I have to say.

Ref-counting with mutable data (which this seems to be replacing?) works the same way in Rust. You need to take an Rc/Arc of RefCell, and calling borrow/borrow_mut on RefCell can fail at runtime.


Only if you use RefCell instead of, say, Cell or Atomic* or RwLock or Mutex.


I had tracing GC in mind.


Interestingly the article misses the comparison with Go, which quite similar to it, and in fact I'd argue - easy, fast, safe.


Go is none of those three. It looks easy on the surface, but it contains a ton of paper cuts and nonsense, people do what looks logical and then every expert Go programmer on HN point and laugh at them. It's not even as fast as Java (for a comparison look at how much careful engineering goes into Java's low latency garbage collectors while Go just hyped the shit out of their inferior version which they had to replace in due course), and calling out from Go is dismal in performance. As for safe, are you kidding me? Go has all this green thread stuff with not a single safety mechanism by default. It's all up to the programmer to implement it by themselves, and they don't get any help, they can't even say that something is immutable. It even regresses on C in that there is no way to declare an enum type. So if you have values representing a bounded set of choices you can't enforce it.

The worst thing is that this travesty of a language will win and lead to decades of stagnation.


Go is easy, decently fast, and reasonably safe. It's a good tool for many problems. Rust is complex, extremely fast, and notably safer than Go. It sounds like Vale is trying to push the envelope of what is possible, which to me, means having a simple yet expressive, extremely fast, and extremely safe language. Go makes compromises in all of those areas (as do many other good languages). I'd argue that Go isn't really near the boundaries in any of those areas, so it's not surprising that they didn't use it as a comparison to illustrate those three aspects of a programming language. It seems Vale's goal is to have their cake and eat it too.


Agreed that Go trades off some performance and safety. I’m not sure what “easy” means exactly, but IMO Go has the shortest learning curves of any language whether “time to productivity” or “time to mastery” and it also has the fastest developer velocity IMO (and I’m coming from Python, which has that reputation as well). It seems like Go is at the boundary for anything worth calling “easy”.


People feel like they have a short learning curve and they master Go. Greenfield programming in Go is an amazingly empowering feeling. The issue is they don't see the issues they create until something implicit, a default value after a refactor or any of Go's paper cuts lead to a production issue.

One central issue is that anything touching concurrency and Go is a tangle of nastiness with implicit effects. But apparently Go is "natively concurrent" and everyone simply prays that the race detector is good enough.

https://eng.uber.com/data-race-patterns-in-go/


Yeah, Go doesn’t have top safety, but it lets you move a lot faster than safer languages. You can spend the extra time finding and fixing most of these bugs and still have time to spare compared to safer languages.

Parallelism does need some design to avoid making a mess (minimize shared mutable state and lock what you can’t minimize and you’re fine). Note also that Rust and other safer languages don’t help much because most shared mutable state is remote and accessed by multiple application processes (e.g., an object in an S3 bucket, a file in an NFS volume, etc). With Rust or with Go, you need to test for these kinds of bugs, but with Go you’ll be able to start your testing sooner (with Rust you’d still be writing the app code).


In what respects is Rust safer than Go? They are both memory safe, and both have safe concurrency options (in addition to unsafe ones).


As we saw in a recent back-and-forth you can come to believe Go is really safe by just having incredibly conservative expectations. If your expectations are low enough Go can meet those low expectations, and you may see a language like Rust as not adding anything since you'd never express anything that's safe (and perhaps faster) in Rust but would be unsafe in Go.


I’m sure you do have a substantive point to make, but all that you actually do in the text of your post is throw shade at Go programmers. I can’t find the back and forth to which you refer.

I'm aware of the potential for data races to lead to data corruption in Go, although as an exploitable issue it seems a bit theoretical. There are also soundness bugs in Rust from time to time (as for example with the whole Pin saga).


> I can’t find the back and forth to which you refer.

https://news.ycombinator.com/item?id=31703732

> I'm aware of the potential for data races to lead to data corruption in Go, although as an exploitable issue it seems a bit theoretical.

Go's language design says it doesn't care about this problem, Rust's language says it eliminates this problem. If you see these as basically the same because you also don't care about the problem, it seems like your original claim (that they're both safe) was simply wrong.

> There are also soundness bugs in Rust from time to time

To be absolutely clear here: Data races in Go are not "bugs". Go is specifically designed not to even be safe if you have a data race which touches compound types. Undefined behaviour, you lose, game over, the language designers have nothing further to say on the matter. If you raise a ticket saying "I had this race and now everything is on fire" in Golang it will get a WONTFIX or whatever the equivalent is.


I think the issue is a bit more subtle than you're suggesting because Rust (as you know) only promises to eliminate the problem in safe code. Go doesn't have the same type infrastructure for stopping you from accidentally sharing data between multiple threads, so it doesn't have the same sharp distinction between 'safe' and 'unsafe' concurrency options. But it's also not as if Go's concurrency story is just 'share data and hope for the best'. Idiomatic Go code should be using channels.

In short, both languages let you write memory unsafe code if you want to, but both discourage it and make it easy not to do so in most cases. Rust discourages it in a more bondage and discipline kind of a way. But it still falls to the programmer to verify the unsafe kernel of their application to their satisfaction. That is, Rust provides a 'here be dragons' warning by forcing the use of an 'unsafe' block, but the language itself doesn't offer any assurances about the correctness of an application's kernel of unsafe code. Modern async Rust doesn't uniformly discourage the use of idioms that make use of unsafe code. See for example the discussion of stack pinning here: https://rust-lang.github.io/async-book/04_pinning/01_chapter... ("A mistake that is easy to make is forgetting to shadow the original variable since you could drop the Pin and move the data ... (which violates the Pin contract).")

I'd advise against inferring that people "don't care" about these problems, etc. etc. This kind of personal stuff just makes it harder to focus on the technical details.


Unless they've made a recent radical change, data races on multiword values (e.g. interfaces and slices) are unsafe in Go. In other GC languages (e.g. Java and C#), data races don't compromise memory safety, but Go made different implementation choices.


Why on Earth is Go plugged into any discussion of languages operating on a much lower level? Go is closer to NodeJS than to Rust from every point of view, it is not C 2.0..


I get the frustration but tbh comparing Go to NodeJS feels just as absurd.

I wish JavaScript had the memory layout guarantees that Go has, that would make it a lot easier to work around memory-based bottlenecks.

And no, the fact that I can use low-levels bindings in NodeJS is not at all similar.


Go has GC and a runtime, which this might not have?




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

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

Search: