5 ms·
What I want for gamedev is not another systems programming language (C++ is good enough for low-level engine code even with all its warts, and it doesn't seem R
by cyber_kinetist 2y ago
What I want for gamedev is not another systems programming language (C++ is good enough for low-level engine code even with all its warts, and it doesn't seem Rust/Zig/etc will replace this easily).
What I really want is something like a lightweight embeddable C# without the GC:
- A strongly-typed scripting language that's easy to bind with a C++-based game engine core (or Rust/Zig/whatever)
- Guarantees sufficient-enough performance while achieving fast compilation for development builds (very important for gamedev iteration). A possible solution for this would be a bytecode VM, but with the option for AOT compilation when exporting your game.
- Comes without a GC. Options could be to just use plain old reference counting, but also something like what Inko does with runtime-checked dangling references or alternatively what Vale does with with generational references.
- [EDIT] Support for value types (like structs in C#)! Really indispensible when you write any "mathy" gamedev code or trying to bind to C++ data structures efficiently.
- The_Colonel 2y agoWhat is the showstopper for GC? There has been a lot of development in the area, e.g. Java's ZGC has maximum pause faster than 1 millisecond (while the typical pause is much shorter). Wouldn't that be good enough for game scripting?
- cyber_kinetist 2y agoThe problem with GC for a game engine is not just plain performance... but it's when you're trying to bind engine objects to the scripting language. From the scripting side you're dealing with GC-like semantics (where object deletion can be deferred indefinitely), but the C++-engine side objects need to adhere to C++ object semantics (whether that be manual / RAII / reference counting). This gap between the two can lead to some "interesting" engine decisions: for example in Unity they have a separate UnityEngine.Object base type separate from the C# runtime System.Object type, which have very different semantics and thus lead to some funny headaches! Note that Godot's scripting language (GDScript) doesn't use GC and instead just uses reference counting implemented in plain C++, and thus were able to unify the object behavior between C++ and the scripting layer. An alternative approach for supporting GC in your game engine, would be to extend C++ on the engine-side to support GC with some black magic (aka Unreal's approach with UObject). But that requires adding an additional hacky transpiler / reflection system on top of C++... which I think has been mostly a net negative in terms of usability (if you've tried using Unreal you'll quickly get what I mean...)
- neonsunset 2y agoNot having GC means either of two things: manual memory management, which contradicts high level nature, or reference counting which is much worse than GC especially in multi-threaded scenarios. Either of the two can use affine memory management hints ala Rust but you still have the issue of sharing the data, which is frequently required in games. Ever allocated an arena, bumped its pointer for many small allocations and then deallocated the entire thing, efficiently? That’s what Gen0 of .NET’s GC is, and it wouldn’t be an issue in a game.
- cyber_kinetist 2y agoGood point... but since you've mentioned that in games you frequently need long-lived shared objects, wouldn't these all go towards Gen1/Gen2 instead of Gen0? I'm not worried about any "local" allocations in a non-GC scripting language since I think these can mainly be taken care of by using RAII (or maybe some explicit support for arena-backed allocations, if you really want more performance), but I'm more worried about objects that live more than a single frame. (Also there's another sibling comment I've wrote that deals with the subtleties of trying to bind a scripting language with a C++ engine core...)
- neonsunset 2y agoYes, long-lived objects will go to Gen1 and 2, but by nature of being long-lived they are also allocated much less frequently. I think it's important to understand the context of where the discourse on "GC in games" comes from - I mentioned in a sibling comment that a lot of perception, at least from Unity side, comes from the fact that in Unity the GC performs (or had performed in the past) really poorly. And, as another comment mentioned, games written in another popular GC language, Java, have little to no avenue in avoiding allocations where even integer operations can lead to data being heap allocated, and requiring the compiler to do a lot of heavy lifting to avoid doing so with escape analysis and packing integers into (compressed) pointer shadows. Actually, I don't think there is much widespread knowledge on how non-Unity C# performs on its own as a language for both the engine and the scripts, at least here on HN. There are Stride3D (rename from Xenko), Fna, MonoGame (which does not use Mono unlike the name suggests), and probably other I don't know about which use .NET 6/8. I'm not aware of GC related issues for any of these games beyond regular aspects of memory management in games that apply regardless of the platform. Something to look for is Unity's migration away from their custom Mono fork to CoreCLR .NET as it should eliminate most if not all remaining GC related issues. I find this interesting as .NET's GC implementations (both WKS, SRV and SRV+DATAS) have not been made with games in mind, and a more game-optimized and frame-pacing-aware GC implementation could be definitely provided, at the cost of complexity of doing such project.
- James_K 2y agoI have always wondered why people, especially game developers, have such a hatred of GC. As I understand it, GC has a higher throughput than malloc/free. This comes at the cost of pauses, but that is essentially just quantising overhead into discrete chunks. In the context of a video game, I always thought you could design a GC that runs once at the end of every frame and thereby causes no stuttering at all and gets to completely ignore all objects allocated and discarded during the frame. A performance win, no? I've only ever come to two conclusions. The first is that the cost of garbage collection is much higher than I estimate, but I have seen no empirical proof of that, and many game engines seem to use garbage collected scripting languages without issue. The second conclusion is that Java's lack of value-types means every program needs to allocate small objects like mad and that degrades performance, which has made most people view GC as slower than it would be if you took a C++ program and swapped the calls to new out with calls to some garbage collected allocator. Do you have any insight on this? What profiling have you done to conclude GC performs worse?
- neonsunset 2y agoA lot of gamedevs have been burned by Unity's Boehm GC (I assume the situation has improved in recent years? Please let me know.). Keep in mind that outside of Minecraft, Java is not popular for writing games where the high-ish level language of choice is usually C# instead, both with Unity and many other open or closed source engines, that either host it as a gamescript language or, lately, implemented entirely in it.
- James_K 2y ago> C# instead, both with Unity and many other open or closed source engines, that either host it as a gamescript language or, lately, implemented entirely in it. And this is what confuses me. The opinions of game developers on this seems utterly baseless, and contradicted by the actual practice of the people who write game engines. I've only ever seen one (quite frankly genius) paper[1] on this that explores the question in a satisfying manner, and it suggests that garbage collection has something like a 17% overhead compared with a malloc call, at the cost of 3 times the memory usage. Not great, but it seems like a video game can afford to triple its memory usage given how it works with resources, and also given that a lot of what's allocated will be large assets which won't need to be counted in the 3× overhead since the represent a small number of objects compared to their size. [1] Matthew Hertz and Emery D. Berger. 2005. Quantifying the performance of garbage collection vs. explicit memory management. SIGPLAN Not. 40, 10 (October 2005), 313–326. https://doi.org/10.1145/1103845.1094836 https://doi.org/10.1145/1103845.1094836
- deleted 2y ago[deleted]
- _cogg 2y agoI've been thinking a lot about embedded scripting lately, to the extent that I've decided to roll my own language. I agree with all your points, but here are some of my own thoughts: It should strive to be familiar for users of the host language, not invent weird new syntax. This is ultimately a personal preference, but what I want is a script-oriented rust: Dispense with some ugly stuff that makes it hard to compile or analyze (borrow checker, macros (maybe), giant core+std libs), but keep enough of the things that make it good (inferred static types, expression oriented, pattern matching). Regarding the "no GC" point: My idea is to just not store any persistent state inside the script engine. Allocate temporary objects in arenas which only live for the duration of a top-level call. If you want persistent state, expose it through bindings. This has the added benefit of making hot reloading trivially easy -- when there's no state, there's nothing for your hot reloading system to break. There are some pretty obvious pitfalls of this. It could totally blow up in my face, so the door is open to adding optional global state and/or garbage collection down the line. I have a very(!) minimal prototype, and I've written enough of a compiler before that I'm confident I can eventually produce something that works. Whether it lives up to my aspirations is a different story. If you want something that actually exists now, maybe look into AngelScript. I haven't personally used it, but it's statically typed and supposedly has good C++ integration.
- cyber_kinetist 2y agoYup I am actually using AngelScript in my current project since it’s currently the least worst option for me! Having the smoothest experience binding with my C++ engine so far. I also started a side project making a strongly-typed scripting language compiler / VM a few months ago, but it’s currently on hold since I’m trying to not do too much yak shaving. But maybe someday…