Hacker News new | ask | show | jobs
by dustbunny 20 days ago
Casey Muratori and Jon Blow have pushed this concept frequently.

They largely don't deeply elaborate, which is sad because I am a professional game developer who is interested in precisely presented knowledge so I can apply it to my work.

My interpretation is that games want to allocate large pools of resources, like GPU buffers, cpu memory, etc, and reuse that memory over and over. Ie: Reinitialize it.

In my experience, RAII is my preferred pattern for certain things (std::lock_guard), and you can almost certainly express the majority of these "uber game dev patterns" using RAII/smart pointers/etc, but the c++ implementation of these "uber game dev patterns" tends to be more complicated (and imo esthetically ugly) compared to really well written C.

These new languages (zig, odin, jai) appear to be attempts to improve C to allow an alternative to C++ that doesn't have the ugly baggage that C++ has.

6 comments

From what I understood, their critique of RAII is twofold: coupling of allocation and initialisation, and enforcement of deallocation. The ease of use of smart pointers makes it tempting to allocate/free of temporary structures even within one single function. Given enough number of such occurrences, it kills performance by a thousand cuts. Also I remember they mentioned it’s not necessary to free memory if you’re about to close your program, because the OS will take the memory back. Obviously you need to gracefully deinitialise some things, like audio or other devices, but that’s beyond the discussion.

As on some references, Ryan Fleury did an episode on Wookash podcast on RAD debugger showing ECS like approach.

IMO, their RAII critique is a but nuanced, but because of their personality the discourse often gets polarising.

Edit: the sibling comment just proved my last point.

most of that criticism only works on C++. rust does enforce RAII but uses stack allocation for locals by default instead of touching the heap so it skips the slow part.

on the other hand there are no constructors (just normal functions) so you cant initialize values in place, only stack allocate and return. i think rust needs to add in place init and change the rules from "always init at declaration site" to "must be initialized at first use" like kotlin.

the big missing piece is custom allocators that let you use something like a bump arena with the same convenience as system malloc. they already exist on nightly but nobody knows when they will land on stable.

honestly thats the biggest problem with rust, they come up with a lot of useful changes but then take ages to stabilize because the core team is overworked. they also have a kind of perfectionist culture as a reaction to all the half baked features shipping in C++.

there should be a stage between nightly and full release where feature is in stable toolchains behind a cfg flag and you get a warning if upstream crates use it. show commitment to shipping it in time but still make it clear that it can change (in minor incompatible ways) before release.

Thanks for this, I’m actually learning Rust (albeit slowly) atm, and still can’t wrap my head around how arena allocs would fit the borrow checker
There are several libraries for doing arena allocation in Rust, e.g. bumpalo: https://crates.io/crates/bumpalo , see the documentation for examples (it's quite easy to use and fits the borrow checker very well). The comments in here regarding Rust are somewhat misleading; Rust doesn't require unstable/nightly features to do this sort of stuff. What's unstable in Rust right now is a standardized interface for generically working with allocators (see https://github.com/rust-lang/rust/pull/157428 for the most recent progress), including support for the containers provided by the stdlib.
I haven't written any serious Rust in a while, but I assume there is some kind of lifetime annotation involved. The "objects" allocated in the arena have to be explicitly given the same lifetime as the arena itself. I have no idea how that looks syntactically.
> honestly thats the biggest problem with rust, they come up with a lot of useful changes but then take ages to stabilize because the core team is overworked

On the contrary, that has been one of Rust's biggest strengths. My impression when reading through stdlib was that they got so many things right, and for that to happen, things need to be thought out properly.

Case in point, it'd be such a shame if they stabilized the allocator API, only for us to forever regret never getting the storage API [1] instead, or vice-versa, depending on which one turns out to be more pragmatic.

> they also have a kind of perfectionist culture as a reaction to all the half baked features shipping in C++.

And that's a good thing! Some people really dislike the constant influx of new features due to the overwhelming complexity it leads to. So if we do have new features, they better be worth it.

[1] https://github.com/rust-lang/rfcs/pull/3446

The ease of use of smart pointers makes it tempting to allocate/free of temporary structures even within one single function. Given enough number of such occurrences, it kills performance by a thousand cuts.

This isn't true and doesn't make any sense. Smart pointers don't need to enter into it. If you need a lot of something you make a vector and allocate once.

Also I remember they mentioned it’s not necessary to free memory if you’re about to close your program, because the OS will take the memory back.

It is an extremely niche scenario to need a program to shut down so much faster that you can't even deallocate memory. If you don't make lots of small allocations in the first place the deallocations won't take any time.

Obviously you need to gracefully deinitialise some things, like audio or other devices, but that’s beyond the discussion.

It's actually a pretty big advantage to destructors to deal with stuff like this as well as memory and locks.

IMO, their RAII critique is a but nuanced, but because of their personality the discourse often gets polarising.

I think it gets polarizing because they are both undeniably sharp programmers but don't have any real evidence of this stuff, they are just grasping at rationalizations.

Casey Muratori and Jon Blow are both hyper-dogmatic "my way or the highway" types, and, crucially, neither of them has built any of the super high fidelity types of game that would require that level of optimisation. They're basically influencer types.
Casey worked on tooling for AAA games that most certainly needed “that level” of optimization.

Jon Blow worked on numerous AAA games that required “that level” of optimization. And he’s one of the very few developers in the last 15 years who have managed to sell more than a million copies of a game running on a scratch built 3D engine.

You can disagree with their opinions, but they certainly have the experience to back those opinions up.

Well, Minecraft sold far more and it's a scratch built 3d engine isn't it? Popularity is certainly not a technical achievement.
Minecraft doesn't use an off the shelf engine, but I wouldn't really call Minecraft scratch built. It's built on top of LWJGL.

Popularity isn't a technical achievement in itself. But what is a technical achievement is that he built a visually impressive 3d engine that worked well enough for over a million people to buy it at a time when almost no one else was doing this. Minecraft was released nearly 10 years earlier--before Unity and Unreal had completely taken over.

I'm also not saying that the witness engine is somehow better than anything else out there, but it's a significant enough technical accomplishment that along with Jon Blow's other work, he has the experience to back up his opinions.

I've also never seen the critique that Jon Blow is just an influencer with no relevant experience from anyone with equivalent relevant experience. I've seen other experienced game devs who disagree with him, but I've never seen them say that he doesn't have the experience to have them.

Only because there are no native OpenGL bindings for Java from Khronos, hence LWJGL.
I don't have any experience with Java game development, but I was under the impression that LWJGL provided more than just OpenGL bindings.
They really are not - there used to be whole schools of thought on how to lay out applications in memory, using the freedom of C before C++ came along, and standardized on a bunch of solutions that are controversial in some circles - stuff like vtables and exceptions are the reason C++ is banned in the kernel.

And with things like Java, you have even less visibility. They made a ton of interviews with oldschool devs, and a most of them did things differently from how Jon and Casey do, but it's not like they disapprove, it's just with modern languages, you don't get the level of control to do this.

Dunno, the Witness had gorgeous lighting. Both have consulted for AAA too.
They might've pushed it, but they certainly didn't invent the idea. Although I'm not 100% familiar with their arguments.

POD - objects with no behavior are pretty popular way of representing data, you can serialize and deserialize them, etc. They have a pretty big caveat in non-GC languages - if you have an object with something like a dynamic array in it, suddenly your stateless POD becomes stateful, and needs RAII otherwise you get a memory leak. No idea how they solve that without GC.

I think those guys both hate C++ so much that they want to dismiss everything about it instead of using all the features that work for them like most people.

My interpretation is that games want to allocate large pools of resources, like GPU buffers, cpu memory, etc, and reuse that memory over and over. Ie: Reinitialize it.

They might say this, but there isn't a good technical rationalization here since anyone can create a global data structure just as easily in C++.

Exactly! The most interesting arguments for Rust that I've ever read have come not from people who hate C++, but precisely from people who are really into C++, because those are the people who actually recognise its shortcomings are are better positioned to give INSIGHTFUL criticism into it.

People who are motivated by a superiority complex or by the wish to impress a herd of twitter followers hardly if ever produce a thought I want to read.

Hate C++? They both use(d) it...
I didn't say they don't know it.
As opposed to using C, Rust, or something else (Jon eventually made Jai but Casey still uses C++). Obviously they don't outright hate it.
They talk about hating it all the time and using some minimal subset because they have to, but the point here is not what these two internet personalities hate, it's that there isn't a technical rationale against being able to use destructors. They are hugely convenient but also completely easy to avoid if someone wants to.
Everyone uses a subset. Bjarne himself says he uses a subset.
See my history regarding my point of view on C or Go.

Yet if my job requires to use C or Go, I like my paychecks, have bills to pay and being proud doesn't pay bills.

Same thing with them, having to use C++ at work, doesn't mean they are big fans of it, indeed the very reason Jai exists is for Jonathan Blow never to use C++ again on his projects.

> Casey Muratori and Jon Blow have pushed this concept frequently

Ah... The school of what I like to call "maximum opinions and minimal evidence". Aggressive arrogant dismissal of anything except their exact view (and for Muratori, you're also "woke" for good measure), coupled with a complete lack of _hard evidence_ to back up their views. In that regard, they're not unlike "investment advice" instagram influencers.

"coupled with a complete lack of _hard evidence_ to back up their views"

Didn't Casey Muratori write a proof of concept windows terminal to highlight Microsoft's substandard implementation? And Jonathan Blow has spent the past decade+ developing his own programming language and funding the development of a game. Seems like they are backing their views.

I can see why you would think that about Jon Blow, though I disagree. But Casey? He has a number of thorough videos with plenty of evidence, and has never struck me as arrogant.
Apparently you're the school of dismissive person who can't even be bothered to look up their technical achievements.
Confusing achievements in one area for evidence in another is literally an appeal to authority fallacy.
You must mean confusing achievements in the area of creating large maintainable and performant systems on the one hand, with achievements in the area of creating large maintainable and performant systems on the other hand?

And let me incude Ryan Fleury here who graduated from Casey-school with honors, he is maybe the most impressive programmer I know. His work on the raddebugger is outstanding.

You're still not anywhere close to the thread's topic. Hero worship isn't evidence.

C++’ish approach to memory management (RAII) - that’s not how systems programming or games (I’m told) tend to work

This is the statement that no one seems to be able to back up. Instead it's fans of some internet programmers saying that they must be right about everything because they shipped a game.

I actually like hearing what they have to say, but they aren't right about this and there is no evidence or explanation here from you, them or anyone else.

Instead you start talking about a third person making a debugger for some reason as if the solution to appealing to authority is more appealing to authority.

> This is the statement that no one seems to be able to back up.

Actually there are many explanations of systemic issues arising from reliance on RAII. Both Jon and Casey are very good explaining the matters.

> Instead you start talking about a third person making a debugger for some reason as if the solution to appealing to authority is more appealing to authority.

A third person (already mentioned in a parent comment), who is creating an extremely snappy and powerful debugger using those exact approaches under discussion.

"Aggressive arrogant dismissal of anything except their exact view"

Man, if only someone around here had even if just an inch of self-awareness...

Casey spends a lot of effort on what he considers an anti-pattern where you're making a huge number of separate objects. I think a lot of this comes from Java, a language where all the user defined types actually are obliged to be heap allocations. If I make a Goose type, and I say I want a Goose, Java will allocate space on the heap for the Goose and put my Goose there, that's really how Java works. If I make a growable array of them ArrayList<Goose>, and add each of 500 geese, that's 500 allocations for geese plus maybe 8 allocations for the ArrayList, so 508 total. Ouch. If making a Goose was itself cheap this overhead hurts badly.

But a lot of languages aren't like that, and so this doesn't translate. Obviously in Rust with Vec<Goose> that's only 8 allocations, each local Goose lives in the stack - or, if you knew up front there were 500 geese, you Vec::with_capacity(500) and it's a single allocation - but similar is true in many languages, Java is an outlier.

Java doesn't mandate the usage of a heap - it can in certain cases avoid doing so and allocate on the stack (escape analysis).

Besides, as mentioned java's allocations are much closer to something like using an arena in a low-level language, then a "slow" malloc. It uses thread-local allocation buffers, where you have a large buffer with a pointer pointing to the start of the free region. Allocation is just a pointer bump, not even needing synchronization since it is per a single thread. As it gets full, the GC moves out still alive objects in the background and resets the buffer.

This is pretty much the most efficient one can be after per-object type arenas and simply not allocating.

You're correct that by an "as if" rule the JIT may in some cases be able to do this trick, but the specification says you're getting a heap allocation and so in most cases, including the example I gave that's exactly what happens.

It doesn't matter that you say this is "much closer to something like using an arena". It's definitely work that we needn't do at all and that's AFAICT that's how Casey ends up over-correcting so badly.

> This is pretty much the most efficient one can be after per-object type arenas and simply not allocating.

Sure. "A C is pretty much the highest grade one can get in this class, after grades A and B".

What "work" do we do needlessly? It's a pointer bump. With reclamation being no work. Comparatively a malloc call will have to do extra work to defragment.

Also, a stack is not a separate hardware element of the RAM, it's not faster than a sufficiently hot part of the heap - it just so happens that the current stack frame is pretty likely to be in cache.

> What "work" do we do needlessly? It's a pointer bump

Any work here was needless. There was no need to do work.

On the allocation happy path there is literally less work in case of Java. Like at least try to get what I'm saying and argue with that - I'm not saying that Java is faster or whatever, but that it has chosen different tradeoffs, and "stack vs heap" is an oversimplified model that doesn't help us understand the real performance impacts.

A pointer bump is a pointer bump, vs a malloc call that will try to find place, do some housekeeping, etc. And your Vec will need at least a single allocation, the backing buffer is not on the stack.

Maybe I'm missing something but when would the 500 geese instances ever be stack allocated in Rust? That comparison seems unfair, the lifetime of that kind of object isn't going to be compatible with stack allocation.

Allocations are really really cheap in Java by the way, so I don't get how 500 allocations would even be an issue.

When you make a local variable with a Goose in Java, that's a heap allocation

    Goose jim = make_a_goose_somehow();  // Java, so jim is on the Heap, no way around it
When you make that variable in Rust...

    let jim : Goose = make_a_goose_somehow();  // Rust, jim is on the stack
Now, if we make a bunch of geese, maybe in a loop, and we put them into our growable array type...

    ArrayList<Goose> geese = new ArrayList<Goose>();
    // ... some loop eventually
      Goose a_goose = somehow_get_this_goose();   // That's an allocation
      geese.add(a_goose);

But in Rust...

    let geese: Vec<Goose> = Vec::new();
    // ... some loop eventually
      let a_goose : Goose = somehow_get_this_goose(); // But this is not
      geese.push(a_goose);
Memory is memory, stack is not a unique hardware element. It just tends to be hot, but so can certain part of "the heap".

Of course this is a toy example, but were the compiler not smart enough (it is surely smart enough in this case) then the "too simple rust" version may actually be slower - it would allocate a Vec on the stack, but only a length, capacity and a pointer is stored there, the actual backing array is on the heap. Then it would create stuff on the stack, and copy over those bytes to the heap, object by object (it's a move).

Meanwhile the java version would have a continuous region of memory, next to it it would have objects, and it would just write pointers to said objects without moving/copying anything.

Surely enough rust is smart enough to optimize out this useless move in this case, but I think you are painting a way too simplified picture here.

What tialaramex is saying is that, in the Java code, there will be a separate allocation for every `a_goose` that gets added to the ArrayList, whereas the Rust code only has a single allocation for the backing storage for the Vec (not counting the resizes that occur as the ArrayList/Vec grows (both cases have ways to preallocate the proper size to avoid any resizes)). The context of this subthread is just to explain why someone who has Java experience might be prone to overcorrection when it comes to avoiding heap allocations.
But one is a "malloc allocation" and the other is a special "java allocation".

The two are apples and oranges and it makes no sense to compare them -- the latter is severalfold cheaper. Of course this comes at a price (read/write barriers, complex runtime, etc).

> The context of this subthread is just to explain why someone who has Java experience might be prone to overcorrection when it comes to avoiding heap allocations

That's what I don't really understand: if anything, someone with Java experience should be prone to avoid heap allocation in something like Rust as it is more expensive on almost every other platform. This is what I'm talking about, and apparently I wasn't clear.

This isn't necessarily true - at least not in a meaningful way - if Goose has a Vec in it, that'll end up on the heap no matter what.
(Sigh)

Fine. If we put a growable array type inside Goose the backing store for such a type would [if needed] live on the heap, unfortunately in Java we're not allowed to use a primitive type like the signed integer here, so lets put a growable array of Doodads where each Doodad is 64 bits.

So now each Java Goose has an ArrayList<Doodad> and yup, each Doodad goes on the heap, and then the ArrayList's backing store goes on the heap with pointers to the Doodads, and then the rest of the Goose goes on the heap too.

In Rust we skip most of those steps for Vec<Doodad> but if there's at least one Doodad we do need to actually have the backing store, the Doodads live directly in that backing store.

Of course if there aren't any Doodads, Java still pays to make the backing store anyway, that's just how Java works again. Rust doesn't do that, an empty Vec<Doodad> is just a dangling pointer, zero capacity, zero length. Since the length and capacity are zero we'll never dereference the pointer.

The story doesn't get better for Java if you make it more convoluted, that's not how any of this works.

To be clear I am not picking nits, nor am I trying to prove that 'language X' is better than Rust - but if you want to mention stack allocation as an advantage, then it has to be general enough to work all the time - in my sibling comment I mentioned arenas, which fix this issue. If you malloc in a function-local arena, both the Goose and the Vec inside it end up in there, and will be automatically freed on return.
Actually, this is my personal opinion, but in Java, allocating an object is just bumping a pointer, so it's much cheaper than doing a malloc in C. I personally don't think the stack is a good place to put temp values and I'm sure neither does Jon Blow, considering he put temporary arena backed allocators right in the language.
Having an arena allocator is completely unrelated. It's one of the features all of these languages out of the Handmade Community seem to have, including Odin (the subject of this thread) and Zig. It's not useless but it's a weird niche to target first.

Local variables don't somehow live in this arena allocator in Odin or Zig, and presumably (I haven't used it) not in Jai either. The obvious place for a local to live is the stack - unless you're Java and you need all your user defined types to live on the heap so that they have unique identities. So that's where locals live in Rust, Zig, Odin, C, C#, Go ...