> The stop-the-world garbage collection is a big pain for games, stopping the world is something you can't really afford to do.
I love this opinion from games programmers because they never qualify it and talk about what their latency budgets are and what they do in lieu of a garbage collector. They just hand wave and say "GC can't work". The reality is you still have to free resources, so it's not like the garbage collector is doing work that doesn't need to be done. What latency budgets are you working with? How often do you do work to free resources? What are the latency requirements there? Even at 144 fps, that's 7ms per frame. If you have a garbage collector that runs in 200us, you could run a GC on every single frame and use less than 3% of your frame budget on the GC pause. I'm -not- suggesting that running a GC on every frame is a good idea or that it should be done, but what I find so deeply frustrating is that the argument that GC can't work in a game engine is never qualified.
edit: wow for once the replies are actually good, very pleased with this discussion.
Disclaimer: I'm not a game developer; but, I've worked on a lot of projects with tight frame time requirements in my time at Netflix on the TVUI team. I also have no experience in Go so I can't comment on the specifics of that garbage collector vs. V8.
I don't think it's necessarily that it "can't" work as much as it takes away a critical element of control from the game developers and the times you find yourself "at the mercy" of the garbage collector is pretty damn frustrating.
The problem we ran into with garbage collection was that it was generally non-deterministic both in terms of when it would happen and how long it would take. We actually added an API hook in our JS to manually trigger GC (something you can do when you ship a custom JS runtime) so we could take at least the "when it happens" out of the picture.
That said, there were often fairly large variances in how long it would take and, while frame time budgets may seem to accommodate things, if you end up in a situation where one of your "heavy computation" frames coincides with a GC that runs long, you're going to get a nasty frame time spike.
We struggled enough that we discussed exposing more direct memory management through a custom JS API so we could more directly control things. We ultimately abandoned that idea for various reasons (though I left over 6 years ago so I have no idea how things are today).
This is basically it. People who have never worked on actual real-time systems just never seem to get that in those environments determinism often matters more than raw performance. I don't know about "soft" real-time (e.g. games, or audio/video) but in "hard" real-time (e.g. avionics, industrial control) it's pretty routine to do things like disable caches and take a huge performance hit for the sake of determinism. If you can run 10% faster but miss deadlines 0.1% more often, that's a fail. It's too easy for tyros to say pauses don't matter. In many environments they do.
I have actually worked on a system where malloc() was forbidden. In fact it always returned null. Buffers were all statically allocated and stack usage was kept to a minimum (it was only a few kB anyways).
The software was shipped with a memory map file so you know exactly what each memory address is used for. A lot of test procedures involved reading and writing at specific memory locations.
It was for avionics BTW. As you may have guessed, it was certified code with hard real time constraints. Exceeding the cycle time is equivalent to a crash, causing the watchdog to trigger a reset.
Malloc was forbidden in a AAA title I worked on. Interestingly enough we also had two (!!!) embedded garbage collected scripting languages that could only allocate in small (32MB, 16 MB) arenas and would assert if they over allocated.
Sounds very similar to NASA C programming guidelines. Each module during the initialization period would allocate its static memory size. Every loop had a upper bound's max iteration to prevent infinite loops.
I think they posted the guidelines and it was a wonderful read about how they developed real time systems.
I was wondering why I couldn't open this pdf on my android pdf viewer and this link gives me cloudflare's captcha. It uses google's ReCaptcha - storefronts, bicycles, semaphores. What a terrible practice. People stop overusing cloudflare!
Because even if you don't use it directly, you might use some code that uses it. And even then, it might not ever be called given the way you are reusing the code, so...a dynamic check is the best way to ensure it never actually gets used.
> Because even if you don't use it directly, you might use some code that uses it.
It seems extremely unlikely that any general purpose code you might adopt, which happens to invoke malloc() at all, would be fit for purpose in such a restricted environment without substantial modification; in which case you would just remove the malloc() calls as well.
> And even then, it might not ever be called given the way you are reusing the code, so...a dynamic check is the best way to ensure it never actually gets used.
In such a restricted environment, it is unlikely you just have unknown deadcode in your project. "Oh, those parts that call malloc()? They're probably not live code and we'll find out via a crash at runtime." That's like the opposite of what you want in a hard realtime system.
So, no — a static, compile/link time check is a strictly superior way to ensure it never gets used.
If you need your system to never dynamically allocate memory, an opaque 3rd party binary blob which might still do that doesn't seem like a valid inclusion.
Again, we're talking about a very restricted hard realtime environment. How do you trust, let alone qualify, a 3rd party binary blob that you know calls malloc(), which is a direct violation of your own design requirements?
Not OP but the most common reason I've seen is that this was an additional restriction that they imposed for their project. The standard library includes a perfectly working malloc. You override it so that you can't end up calling it accidentally, either explicitly (i.e. your brain farts and you do end up malloc()-ing something) or implicitly (i.e by calling a function that ends up calling malloc down the line). The latter is what happens, and surprisingly easy and often. Not all libraries have the kind of documentation that glibc has, nor can you always see the code.
I’d be interested in knowing more if you care to write more about this. I’m currently costing/scoping out what’s required in writing software at that level but don’t really have some real industry insight into what other people do
Static memory is something we already do but we’re pretty interested whether industry actually adopts redundant code paths, monitors, to what extent watchdogs (how many cycles can be missed?), etc.
I suggest you take a look at the books by Michael Pont and the offerings through his company "SafeTTy Systems". His/Company's work deals with hard real-time systems with all the associated standards.
Wow - disabling the caches is an extreme measure. I get it though. After doing that, you can (probably) do cycle counting on routines again. Just like the 80s or earlier.
There's probably enough jitter in the memory system and in instruction parallelism that accurate cycle counting will still be challenging.
Also, you probably want some padding so that newer versions of the CPU can be used without too much worry. It's possible for cycle counts of some routines to increase, depending on how new chips implement things under the hood.
[says a guy who was counting cycles, in the 1980s :-)]
I don't. Maybe we're thinking about different kind of caches, but if these are transparent, no-performance-impact caches, then why wouldn't you prove the system works well with caches off (guarantee deadlines are met), then enable caches for opportunistic power gains?
> if these are transparent, no-performance-impact caches
If there were no performance impact, there would be no point. I'm not just being snarky; there's an important point here. Caches exist to have a performance impact. In many domains it's OK to think about caching as a normal case, and to consider cache hit ratio during designs. When you say "no performance impact" you mean no negative performance impact, and that might be technically true (or it might not), but...
But that's not how a hard real-time system is designed. In that world, uncached has to be treated as the normal case. Zero cache hit ratio, all the time. That's what you have to design against, even counting cycles and so on if you need to. If you're designing and testing a system to do X work in Y time every time under worst-case assumptions, then any positive impact of caches doesn't buy you anything. Does completing a task before deadline because of caching allow you to do more work? No, because it's generally considered preferable to keep those idle cycles as a buffer against unforeseen conditions (or future system changes) than try to use them. Anything optional should have been passed off to the non-realtime parts of the larger system anyway. There should be nothing to fill that space. If that means the system was overbuilt, so be it.
The only thing caches can do in such a system is mask the rare cases where a code path really is over budget, so it slips through testing and then misses a deadline in a live system where the data-dependent cache behavior is less favorable. Oops. That's a good way for your product and your company to lose trust in that market. Once you're designing for predictability in the worst case, it's actually safer for the worst case to be the every-time case.
It's really different than how most people in other domains think about performance, I know, but within context it actually makes a lot of sense. I for one am glad that computing's not all the same, that there are little pockets of exotic or arcane practice like this, and kudos to all those brave enough to work in them.
While you might test your hard real-time requirements with caches disabled, there's still reason to run the code with caches afterwards.
E.g. errors that didn't match a branch or input scenario during testing which would go over budget without cache, but with cache might prevent a crash.
Another could be power consumption, latency optimization, or improvement of accuracy. E.g. some signal analysis doesn't work at all if the real-time code is above some required Nyquist threshold, but faster performance improves the maximum frequency that can be handled, improving accuracy.
You could be right on some of those. That didn't seem to be the prevailing attitude when I worked in that area, but as I said that was a long time ago - and it was in only a few specific sub-domains as well.
Forgot to mention: cache misses can be more expensive than uncached accesses, so testing with caches off and then turning them on in production can be a disaster if you hit a cache-busting access pattern. Always run what you tested.
Who will provide a guarantee that the caches are truly transparent and will not trigger any new bugs?
Essentially, you would need to prove the statement "if a system works well with caches off, then it works well with caches on" to the satisfaction of whatever authority is giving you such stringent requirements.
You'd have to very solidly prove that in the wors-case a cache only ever make execution time equal to or faster than a processor not using cache and never causes anything to be slower.
Even that is not enough. The cache may make everything faster, but it could lead to higher contention on a different physical resource slowing things down there. The cache cannot be guaranteed to prevent that.
I didn't make up the terminology. It was already in common use at least thirty years ago when I was actively working in that domain. To simplify, it's basically about whether a missed deadline is considered fatal or recoverable. That difference leads to very different design choices. Perhaps some kinds of video software is hard real-time by that definition, but for sure a lot of it isn't. I'd apologize, but it was never meant to be pejorative in the first place and being on one side of the line is cause for neither pride nor shame. They're just different kinds of systems.
What percentage of the market is A/V build to actual hard real-time standards, and not expected to run on devices that can't provide it (so no PCs with normal OSes, no smartphones)? For the vast majority, soft real-time is fine, since an occasional deadline-miss results in minor inconvenience, not property damage, injury or death.
I assume some dedicated devices are more or less hard real time, due to running way simpler software stacks on dedicated hardware.
I take it that scarcely anyone here has written software for video switchers, routers, DVEs, linear editors, audio mixers, glue products, master control systems, character generators, etc. etc. Missing a RT schedule rarely results in death, but you'd think so given the attitude from the customer. That's a silly definition for it.
There's a whole world out there of hard real time, the world is not simply made up of streaming video and cell phones.
The cool thing on HN is you can get down voted for simply making that observation. It's a sign of the times I'm afraid.
I actually have written software for video routers and character generators. We didn't consider them hard real time, though I wouldn't claim that such was standard industry usage.
For example, if you're doing a take, you have to complete it during the blanking interval, but usually the hardware guarantees that. In the software, you want you take to happen in one particular vertical blanking interval (and yes, it really is a frame-accurate industry). But if you miss, you're only going to miss by one. We didn't (so far as I know) specify guarantees to the customer ("If you get your command to the router X ms before the vertical interval, your take will happen in that vertical"), so we could always claim that the customer didn't get the command to the router in time. Again, so far as I know - there may have been guarantees given to the customer, but I didn't know about them.
But that was 20 years ago, back in the NTSC 525 days.
Nice name, by the way. Do you know of any video cards that will do a true Porter & Duff composite these days? I recall looking (again, 20 years ago) at off-the-shelf video cards, and while they could do an alpha composite, it wasn't right (and therefore wasn't useful to us).
I work on an open-source music sequencer (https://ossia.io) and no later than two days ago I had a fair amount of mails with someone who wanted to know the best settings for his machine to not have any clicks during the show (which are the audio symptoms of "missed deadline"). I've met some users who did not care, but the overwhelming majority does, even for a single click in a 1-hour long concert.
If it's running in a consumer OS (not a RT one) and it counts on having enough CPU available to avoid missing the deadline, that's exactly what soft-realtime is.
Compare your “not a single click in an hour [for quality reason]” to a “not a single missed deadline in 30 years of the life expectancy of a plane, on a fleet of a few thousands planes [for safety reasons]”. That's the difference of requirements between hard and soft RT.
I did some soft real-time (video decoding) and I have a friend working on hard real-time (avionics) and we clearly didn't worked in the same world.
Yeah. To me, hard real time is when you count cycle (or have a tool that does it for you), to guarantee that you make your timing requirements. We never did that.
RT video/audio failing never results in death. Where as failures in "avionics, industrial control" absolutely can / do. That seems to be where OC was drawing the line.
Seems to be a common distinction, although GP is right with the addition that the production side of things is more demanding (and at least would suffer financial damage if problems occur to often) than the playback side formed by random consumer gear, and has some, especially low-level/synchronization-related, gear to hard standards. But often soft is enough, as long as it's reliable enough on average.
>For the vast majority, soft real-time is fine, since an occasional deadline-miss results in minor inconvenience, not property damage, injury or death.
A "minor inconvenience" like a recording session going wrong, a live show with stuttering audio, skipped frames in a live TV show, and so on?
Most professional recording studios are using consumer computer hardware that can't do hard realtime with software that doesn't support hard realtime.
People like deadmau5, Daft Punk, Lady Gaga all perform with Ableton Live and a laptop or desktop behind their rig. If it were anything more than a minor inconvenience, these people wouldn't use this.
It's very unlikely to have audio drop outs, a proper setup will basically never have them. But still if you have one audio dropout in your life, you're not dead, your audience isn't dead, a fire doesn't start, a medical device doesn't fail to pump, and so on.
And yes you can badly configure and system, but the point is you can't configure these to be 100% guaranteed, 99.99% is perfectly fine.
Edit: Sometimes people call these "firm" realtime systems. Implying the deadline cannot be missed for it to operate, but also that failure to meet deadlines doesn't result in something serious like death (e.g in a video game you can display frames slower than realtime and it kind of works but feels laggy, however you cannot also slow down the audio processing because you'll a lowered pitch, so you have to drop the audio.)
As long as the individual event happens seldom enough few of these actually are a big problem. Soft real-time being allowed to blow deadlines doesn't mean it can't be expected to have a very high rate of success (at least that's the definitions I've learned), and clearly a sufficiently low rate of failure is tolerated. There's a vast difference between "there's an audio stutter every day/week/month/..." and "noticeably stuttering audio". The production side is obviously a lot more sensitive about this than playback, but will still run parts e.g. on relatively normal desktop systems because the failure rate is low enough.
The production side usually renders the final audio mix off-line, so no real-time requirements there for getting optimum sound quality. I'd say the occasional rare pop or stutter is worse to have during a live performance than when mixing and producing music.
There's probably 500+ successful GC based games on the Steam store another 100000 to a million hobby games doing just fine with GC.
I started game programming on the Atari 800, Apple 2, TRS-80. Wrote NES games with 2k of ram. I wrote games in C throughout the 90s including games on 3DO and PS1 and at the arcade.
I was a GC hater forever and I'm not saying you can ignore it but the fact that Unity runs in C# with GC and that so many quality and popular shipping games exist using it is really proof that GC is not the demon that people make it out to be
Some games made with GC include Cuphead, Kerbal Space Program, Just Shapes & Beats, Subnautica, Ghost of a Tale, Beat Saber, Hollow Knight, Cities: Skylines, Broforce, Ori and the Blind Forest
And a very important one, Minecraft, atleast the Java version. It doesn't matter until you are running a couple hundred mods with lots of blocks and textures, when the max memory allocated gets saturated and it stutters like hell.
> and the times you find yourself "at the mercy" of the garbage collector is pretty damn frustrating.
You're still at the mercy of the malloc implementation. I've seen some fairly nasty behaviour involving memory leaks and weird pauses on free coming from a really hostile allocation pattern causing fragmentation in jemalloc's internal data.
Which is why you generally almost never use the standard malloc to do your piecemeal allocations. A fair number of codebases I've seen allocate their big memory pools at startup, and then have custom allocators which provide memory for (often little-'o') objects out of that pool. You really aren't continually asking the OS for memory on the heap.
In fact, doing that is often a really bad idea in general because of the extreme importance of cache effects. In a high-performance game engine, you need to have a fine degree of control over where your game objects get placed, because you need to ensure your iterations are blazingly fast.
Doesn’t this just change semantics? Whatever custom handlers you wrote for manipulating that big chunk of memory are now the garbage collector. You’re just asking for finer grained control than what the native garbage collection implementation supports, but you are not omitting garbage collection.
Ostensibly you could do the exact same thing in e.g. Python if you wanted, by disabling with the gc module and just writing custom allocation and cleanup in e.g. Cython. Probably similar in many different managed environment languages.
I mean, nobody is suggesting they leave the garbage around and not clean up after themselves.
But instead what you can do is to reuse the "slots" you are handing out from your allocator's memory arena for allocations of some specific type/kind/size/lifetime. If you are controlling how that arena is managed, you will find yourself coming across many opportunities to avoid doing things a general purpose GC/allocator would choose to do in favor of the needs dictated by your specific use case.
For instance you can choose to draw the frame and throw away all the resources you used to draw that frame in one go.
The semantics matter. A lot of game engines use a mark-and-release per-frame allocation buffer. It is temporary throwaway data for that frame's computation. It does not get tracked or freed piecemeal - it gets blown away.
Garbage collection emulates the intent of this method with generational collection strategies, but it has to use a heuristic to do so. And you can optimize your code to behave very similarly within a GC, but the UI to the strategy is full of workarounds. It is more invasive to your code than applying an actual manual allocator.
> A lot of game engines use a mark-and-release per-frame allocation buffer.
I've heard of this concept but a search for "mark-and-release per-frame allocation buffer" returned this thread. Is there something else I could search?
It’s just a variation of arena allocation. You allocate everything for the current frame in an arena. When the frame is complete. You free the entire arena, without needing any heap walking.
A generational GC achieves a similar end result, but has to heuristically discover the generations, whereas an arena allocator achieves the same result deterministically And without extra heap walking.
Linear or stack allocator are other common terms. Just a memory arena where an allocation is just a pointer bump and you free the whole buffer at once by returning the pointer to the start of the arena.
Getting rid of this buffer is literally nothing. There is no free upon the individual objects needed. You just forget there was anything there and use the same buffer for the next frame. Vs. Waiting for a GC to detect thousands of unused objects in that buffer and discard them, meanwhile creating a new batch of thousands of objects and having to figure out where to put those.
You can do many things in many languages. You may realize in the process that doing useful things is made harder when your use case is not a common concern in the language.
C's free() gives memory back to the operating system(1), whereas, as a performance optimization, many GCd languages don't give memory back after they run a garbage collection (see https://stackoverflow.com/questions/324499/java-still-uses-s...). Every Python program is using a "custom allocator," only it is built in to the Python runtime. You may argue that this is a dishonest use of the term custom allocator, but custom is difficult to define (It could be defined as any allocator used in only one project, but that definition has multiple problems). The way I see it, there are allocators that free to the OS and those that don't or usually don't (hereafter referred to as custom).
In C, a custom allocator conceivably could be built into, say, a game engine. You might call ge_free(ptr) which would signal to the custom allocator that chunk of memory is available and ge_malloc() would use the first biggest chunk of internally allocated memory, calling normal malloc() if necessary.
Custom allocators in C are a bit more than just semantics, and affect performance (for allocation-heavy code). Furthermore, they are distinct from GC, as they can work with allocate/free semantics, rather than allocate/forget (standard GC) semantics.
Yes, one could technically change any GCd language to use a custom allocator written by one's self. But Python can't use allocate/free semantics (so don't expect any speedup). Python code never attempts manual memory management, (i.e. 3rd party functions allocate on the heap all the time without calling free()) because that is how Python is supposed to work. To use manual memory management semantics in Python, you would need to rewrite every Python method with a string or any user defined type in it to properly free.
(1) malloc implementations generally allocate a page at a time and give the page back to the OS when all objects in the page are gone. ptr = malloc(1); malloc(1); free(ptr); doesn't give the single allocated page back to the OS.
Python is a bad example to talk about gc, because it uses different garbage collector than most of languages. It is also the primary reason why getting rid of GIL and retaing performance is so hard. Python uses reference counters and as soon as the reference count drops to 0 it immediately frees the object, so in a way it is more predictable. It has also a traditional GC and I guess that's what was mentioned you can disable it. The reason for it is that reference count won't free memory of there is a loop (e.g. object A references B and B references A, in that case both have reference count 1 even though nothing is using them), do that's where the traditional GC steps in.
It's not a performance optimisation not to give space back. GCs could easily give space back after a GC if they know a range (bigger than a page) is empty, it's just that they rarely know it is empty unless they GC everything, and even then there is likely to be a few bytes used. Hence the various experiments with generational GC, to try to deal with fragmentation.
Many C/C++ allocators don't release to the OS often or ever.
That's true, and it's why the alternative to GC is generally not "malloc and free" or "RAII" but "custom allocators."
Games are very friendly to that approach- with a bit of thought you can use arenas and object pools to cover 99% of what you need, and cut out all of the failure modes of a general purpose GC or malloc implementation.
Due the low throughput of Go's GC (which trades a lot of it in favor of short pause duration), you risk running out if memory if you have a lot of allocations and you don't run your GC enough times.
In a context where you don't allocate memory, you lose a lot of those (for instance, you almost cannot use interfaces, because indirect calls cause parameters to those calls to be judged escaping and unconditionally allocated on the heap).
Go is a good language for web backend and other network services, but it's not a C replacement.
If you allocate a large block of memory manually at the start of the program, then trigger the GC manually when it suits you, won't you get the best of both worlds?
You can't call native libraries without going through cgo. So unless you don't want to have audio, draw text and have access to the graphic APIs, you'll need cgo, which is really slow due to Go's runtime. For game dev, that's a no go (pun intended).
Additionally, the Go compiler isn't trying really hard at optimizing your code, which makes it several times slower on a CPU-bound task. That's for a good reason: because for Go's usecase, compile-time is a priority over performances.
Saying that there is no drawbacks in Go is just irrational fandom…
Go was pushed as a C replacement, but very few C programmers switched to it, it seems like it took hearts of some of Python, Ruby, Java etc programmers.
I would very much prefer a stripped down version of Go used for these situations rather than throwing more C at it. The main benefits of using Go are not the garbage collection, its the tooling, the readability (and thus maintainability) of the code base, the large number of folks who are versatile in using it.
Large user base? C is number 2. Go isn't even in the top 10.[1]
Tooling? C has decades of being one of the most commonly used languages, and a general culture of building on existing tools instead of jumping onto the latest hotness every few months. As a result, C has a very mature tool set.
Unfortunately the excellent standard library is a major benefit of Go, and it uses the GC, so if you set GOGC=off you're left to write your own standard library.
I would also like to see a stripped-down version of Go that disables most heap allocations, but I have no idea what it would look like.
I'd be willing to wager that C programmers would be more comfortable working with a Golang codebase than Golang programmers would be working with a C codebase.
There may be more "C programmers" by number but a Golang codebase is going to be more accessible to a wider pool of applicants.
In my experience it takes a few days for a moderate programmer to come up to speed on Go, whereas it takes several months for C. You need to hire C programmers for a C position, you can hire any programmers for a Go position.
How do people learn C without knowing about manual memory management? They learn about it as they learn the language. This can be done in any language that allows for manual memory management (and most have much better safeguards and documentation than C, which has a million ways to shoot yourself in the foot)
You’re writing in a much improved C. Strong type system (including closures/interfaces/arrays/slices/maps), sane build tooling (including dead simple cross compilation), no null-terminated strings, solid standard library, portability, top notch parallelism/concurrency implementation, memory safety (with far fewer caveats, anyway), etc. Go has it’s own issues and C is still better for many things, but “Go with manually-triggered GC” is still far better than C for 99.9% of use cases.
Go’s compiler is not at all optimized for generating fast floating point instructions like AVX and its very cumbersome to add any kind of intrinsics. This might not matter for light games but an issue when you want to simply switch to wide floating point operations to optimize some math.
GCC can compile both C and Go. I searched for benchmarks but found none for GCC 9 that compares the performance of C and Go. Do you have any sources on this?
I don’t have a source, but it’s common knowledge in the Go community. Not sure how GCC works, but it definitely produces slower binaries than gc (the standard Go compiler). There are probably some benchmarks where this is not the case, but the general rule is that gcc is slower. gc purposefully doesn’t optimize as aggressively in order to keep compile times low.
Personally I would love for a —release mode that had longer compile times in exchange for C-like performance, but I use Python by day (about 3 orders of magnitude slower than C) so I’d be happy to have speeds that were half as fast C. :)
Yes, the idea is that you must invoke the GC when you’re not in a critical section. Alternatively you can just avoid allocations using arenas or similar. (You can use arrays and slices without the GC).
To make sure I understand, is this an accurate expansion of your comment?
Yes it would leak, to avoid leaking you could invoke the GC when you’re not in a critical section. Alternatively, if you don't use maps and instead structure all your data into arrays, slices and structs, you can just avoid allocations using arenas or similar. (You can use arrays and slices without the GC, but maps require it).
Yes, that is correct. Anything that allocates on the heap requires GC or it will leak memory. Go doesn’t have formal semantics about what allocates on the heap and what allocates on the stack, but it’s more or less intuitive and the toolchain can tell you where your allocations are so you can optimize them away. If you’re putting effort into minimizing allocations, you can probably even leave the GC on and the pause times will likely be well under 1ms.
Speedrunners thank you for reusing objects! I'm certain that decisions like this are what lead to interesting teleportation techniques and item duplications. Games wouldn't be the same without these fun Easter eggs!
Once I wrote a very small vector library in JS for this very reason: almost all JS vector libraries out there tend to dynamically create new vectors for every vector binary operation, this makes JS GC go nuts. It's also prohibitively expensive to dynamically instantiate typedarray based vectors on the fly, even though they are generally faster to operate on... most people focus on fixing the latter in order to be able to use typedarrays by creating vector pools (often as part of the same library), but this creates a not-insignificant overhead.
Instead my miniature library obviated pools by simply having binary operators operate directly on one of the vector objects passed to it, if more than one vector was required for the operation internally they would be "statically allocated" by defining them in the function definition's context (some variants i would also return one of these internal vectors - which was only safe to use until a subsequent call of the same operator!).
The result this had on the calling code looked quite out of place for JS, because you would effectively end up doing a bunch of static memory allocation by assigning a bunch of persistent vectors for each function in it's definition context, and then you would often need to explicitly reinitialize the vectors if they were expected to be zero.
... it was however super fast and always smooth - I wish it was possible to turn the GC off in cases like this when you know it's not necessary. It was more of a toy as a library, but i did write some small production simulations with it - i'm not sure how well the method would extend to comprehensive vector and matrix libraries, I think the main problem is that most users would not be willing to use a vector library this way, because they want to focus on the math and not have to think about memory.
You wouldn't have this still up somewhere like GH would you? I'm currently writing a toy ECS implementation and have somewhat similar needs, and I've been trying to build up a reference library of sorts covering novel ways of dealing with these kind of JS issues
No sorry, this is from more than 5 years ago and it was never FOSS, but I only implemented rudimentary operators anyway, you could easily adapt existing libraries or write your own, the above concept is more valuable than any specific implementation details... the core concept being, never implicitly generate objects, e.g operate directly on parameter objects, or re-use them for return value, or return persistent internal objects (potentially dangerously since they will continue to be referenced and used internally).
All of these ideas require more care when using the library though.
Thank you though, what you've written here is very useful -- you're describing things I'm immediately recognising in what I'm doing. As I say, it's just a small toy (and I think one that would definitely be easier in a different language, but anyway...). At the minute I'm actually at the point where I'm preallocating and persisting a set of internal objects, and at a very very small scale it's ok, but each exploration in structure starts to become manageable pretty quickly.
You can see most operations act on the Vector and there are some shared temporary variables that have been preallocated. If you look through some of the other parts you can see closures used to capture pre-allocated temporaries per function as well.
This kind of place orientated programming can make the actual algorithm very hard to follow. I really hope that JS gets custom value types in the future.
Go’s GC is low latency and it allows you to explicitly trigger a collection as well as prevent the collector from running for a time. I would wager that the developer time/frustration spent taming the GC would be more than made up for by the dramatic improvement in ergonomics everywhere else. Of course, the game dev libraries for Go would have to catch up before the comparison is valid.
It’s factually correct. My other post got downvoted for pointing out that not every GC is optimized for throughout. Seems like I’m touching on some cherished beliefs, but not sure exactly which ones.
I know this subject quite well and I will later publish a detailed article.
The real run-time cost of memory management done well in a modern game engine written without OOP features is extremely low.
We usually use a few very simple specialized memory allocators, you'd probably be surprised by how simple memory management can be.
The trick is to not use the same allocator when the lifetime is different.
Some resources are allocated once and basically never freed.
Some resources are allocated per level and freed all at once at the end.
Some resources are allocated during a frame and freed all at once when a new frame starts.
And lastly, a few resources are allocated and freed randomly, and here the cost of fragmentation is manageable because we're talking about a few small chunks (like network packets)
+1. We have a large Rust code base, and we forbid Vec and the other collections.
Instead, we have different types of global arenas, bump allocators, etc. that you can use. These all pre-allocate memory once at start up, and... that's it.
When you have well defined allocation patterns, allocating a new "object" is just a "last += 1;` and once you are done you deallocate thousands of objects by just doing `last -= size();`.
That's ~0.3 nanoseconds per allocation, and 0.x nano-seconds to "free" a lot of memory.
For comparison, using jemalloc instead puts you at 15-25 ns per allocation and per deallocation, with "spikes" that go up to 200ns depending on size and alignment requirements. So we are talking here a 100-1000x improvement, and very often the improvement is larger because these custom allocators are more predictable, smaller, etc. than a general purpose malloc, so you get better branch prediction, less I-cache misses, etc.
Do you use any public available crate for those allocators? Would love to take a look. I'm currently trying to write a library for no-std which requires something like that. I currently have written a pool of bump allocators. For each transaction you grab an allocator from the pool, allocate as many objects from it as necessary, and then everything gets freed back to the pool. However it's a bit hacky right now, so I'm wondering whether there is already something better out there.
> Do you use any public available crate for those allocators?
Not really, our bump allocator is ~50 LOC, it just allocates a `Box<[u8]>` with a fixed size on initialization, and stores the index of the currently used memory, and that's it.
We then have a `BumpVec<T>` type that uses this allocator (`ptr`, `len`, `cap`). This type has a fixed-capacity, it cannot be moved or cloned, etc. so it ends up being much simpler than `Vec`.
Exactly this. Writing a basic allocator can give you a significant bang for your buck. Hard-coding an application specific arena allocator is trivial. The harder part is being diligent enough to avoid use after free/dangling pointers.
I'm a huge Java nerd. I love me some G1/Shenandoah/ZGC/Zing goodness. But once you're writing a program that to the point that you're tuning memory latency in many games anyway, baking in your application's generational hypothesis is pretty easy. Even in Java services you'll often want to pool objects that have odd lifetimes.
This is exactly what's continually drawing me to Zig for this sort of thing. Any function that requires memory allocation has to either create a new allocator for itself or explicitly accept an existing one as an argument, which seems to make it a lot easier/clearer to manage multiple allocators for this exact use case.
This sounds very interesting. Please do write an article abput the topic! Seems like you would introduce specific allocators for maybe different games.
I buy this - specialized memory allocators, after all I've used mbufs. So which languages would be good or recommended for coercing types when using memory allocators?
It would be interesting to see what the impact of adding hints about allocation lifetime to malloc would be. I have to suspect someone has already written that paper and I just don't know the terminology to look for it.
I'm reminded of an ancient 8031 C compiler. It didn't support reentrancy. Which seems like a bad thing until you realize that the program could be analyzed as a acyclic graph. The compiler then statically allocated and packed variables based on the call tree. Programs used tiny amounts of memory.
Given the problems people have with Rust and the borrow checker, I would guess that people will not accurately predict how long an allocation will live (it's a similar issue with profiling---programmers usually guess wrong about where the time is being spent).
as an example point, the Go garbage collector clears heaps of 18gb in sub-millisecond latencies. If I'm understanding the problem at hand (maybe I'm not!), given an engine running at a target framerate of 144 frames per second, you're working with a latency budget of about 7ms per frame. Do you always use all 7ms, or do you sometimes sleep or spin until the next frame to await user input?
We can also look at it from the other direction: if your engine is adjusting its framerate dynamically based on the time it takes to process each frame, and you can do the entire work for a frame in 10ms, does that give you a target of 100 fps? If you tack on another half millisecond to run a GC pause, would your target framerate just be 95 fps?
And what do you do when the set of assets to be displayed isn't deterministic? E.g., an open world game with no loading times, or a game with user-defined assets?
There is no general answer to this question.
Frame latency, timing and synchronization is a difficult subject.
Some games are double or triple buffered.
Rendering is not always running at the same frequency as the game update.
The game update is sometimes fixed, but not always.
I've had very awful experience with GC in the past, on Android, the game code was full C/C++ with a bit of Java to talk to the system APIs, I had to use the camera stream.
At the time (2010) Android was still a hot mess full of badly over engineered code.
The Camera API was simply triggering a garbage collect about every 3-4 frames, it locked the camera stream for 100ms (not nanoseconds, milliseconds!)
The Android port of this game was cancelled because of that, it was simply impossible to disable the "feature".
You didn't have a bad experience with GC in the past, you had a bad experience with a single GC implementation, one which was almost certainly optimized for throughput and not latency and in a language that pushes you toward GC pressure by default. :)
I never worked with Unity myself but I worked with people using Unity as their game engine, they all had problems with stuttering caused by the GC at some point.
You can try to search Unity forums about this subject, you'll find hundreds or thousands of topics.
What really bothers me with GC is that it solves a pain I never felt, and creates a lot of problems that are usually more difficult to solve.
This is a typical case of the cure being (much) worse than the illness.
What is Unity’s STW time? Is it optimized for latency? If not, you’re using the wrong collector. The pain point it solves is time spent debugging memory issues and generally a slower pace of development, but of course if you’re using a collector optimized for throughout, you’ll have a bad time. Further, a low-latency GC will pay off more for people who haven’t been using C or C++ for 10 years than those who have.
It is very nice to say that, in theory, a GC could work very well for performance demanding games. But until someone builds that GC, in an environment where everything else is suitable for games also, it is academic. We can't actually build a game with a theoretical ecosystem.
Of course, this is a theoretical conversation. My point is that you don’t refute the claim, “low latency GCs are suitable for game dev” with “I used a high latency GC and had a bad time”. Nor do you conclude that GCs inherently introduce latency issues based on a bad experience with a high latency GC. There is neither theory nor experiment that supports the claim that GCs are inherently unsuitable for game dev.
"Clearing an 18GB heap" that's full of 100MB objects that are 99% dead is different than clearing an 18GB heap of 1KB objects that are 30% (not co-allocated, but randomly distributed across the whole heap).
this is exactly the kind of dismissive hand-waving that is frustrating. The 18gb heap example is from data collected on production servers at Twitter. Go servers routinely juggle tens of thousands of connections, tens of thousands of simultaneous user sessions, or hundreds of thousands of concurrently running call stacks. We're essentially never talking about 100mb objects since the vast majority of what Go applications are doing is handling huge numbers of small, short-lived requests.
I'm not a game developer, just a programming language enthusiast with probably above average understanding of how difficult the problem this is.
Can you point out in the post where they expand on my point? The only this I see is this:
> Again we kept knocking off these O(heap size) stop the world processes. We are talking about an 18Gbyte heap here.
which is exactly my point - even if you remove all O(heap size) locks, depending on the exact algorithm it might still be O(number of objects) or O(number of live objects) - e.g. arena allocators are O(1) (instant), generational copying collectors are O(live objects), while mark-and-sweep GCs (including Go's if I understand correctly after skimming over your link) are O(dead objects) (the sweeping part). Go's GC seems to push most of that out of Stop-The-World pause, instead it offloads it to mutator threads instead... Also, "server with short-lived requests" is AFAIK a fairly good usecase for a GC - most objects are very short-lived, so it would be mostly garbage with simple live object graph...
Still, a commendable effort. Could probably be applied to games as well, though likely different specific optimisations would be required for their particular usecase. I think communication would be better if you expanded on this (or at least included the link) in your original post.
There are plenty of known GC algorithms with arbitrarily bounded pause times. They do limit the speed at which the mutator threads can allocate, but per-frame allocations can be done the same way they are done in a non-GC'd environment (just allocate a pool and return everything to it at the end of each frame), and any allocations that are less frequent will likely be sufficiently rare for such algorithms.
I know people who have written games in common-lisp despite no implementations having anything near a low-latency GC. They do so by not using the allocator at all during the middle of levels (or using it with the GC disabled, then manually GCing during level-load times).
Best comment here. I have spent some time evaluating Go for making games. Considering the fact that for 95% of games it doesn't really matter if the language has a GC it could have been a very nice option.
Currently CGO just sucks. It adds a lot of overhead. The next problem is target platforms. I don't have access to console SDKs but compiling for these targets with Go should be a major concern.
Go could be a nice language for making games but with the state it is in the only thing is good for is hobby desktop games.
Go's GC has latencies that would be good enough for a great many games, but its throughput might become problematic, and in general, as a language it is problematic for the kinds of games that would worry about GC pauses, since FFI with c libraries is very expensive. If that could be solved Go might be very appealing for many many games.
I worked as a games programmer for 8 years in C/C++, and spent an accumulated 2 years just doing optimisation, during the time of 6th and 7th generation consoles. Freeing resources in a deterministic manner is important for the following reasons:
FRAME RATE: having a GC collect at random frames makes for jerky rendering
SPEED: Object pools allow reuse of objects without allocing/deallocing, and can be a cache-aligned array-of-structs. Structs-of-arrays can be used for batch processing large volumes of primitives.
https://en.wikipedia.org/wiki/AoS_and_SoA
RELIABILITY: This is probably applicable to the embedded realm too, but if you can't rely on virtual memory (because the console's OS/CPU doesn't support it, or once again you don't want the speed impact) then you need to be sure that allocations always succeed from a fixed pool of memory. Pre-allocated object pools, memory arenas, ring buffers etc. are a few of ways to ensure this.
There's probably a lot more, but those are the reasons that jump out at me.
you can turn off the gc in Go and run it manually. you can also write cache-aligned arrays of structs in Go if you want to. you can allocate a slab and pull from it if you want to. the existence of a GC doesn't preclude these possibilities.
The existence of a GC, even when it can be turned off, does preclude a great many other possibilities, in practice. One issue which is nearly universe, and extra bad in Go, is the extra cost of FFI with C libs, which is necessary in games to talk to opengl, or sdl2, or similar.
If you aren't going to use the GC, then you open up a lot of other performance opportunities by just using a language that didn't have one in the first place.
>you can turn off the gc in Go and run it manually
And if you run the GC manually, you really don't know how long it will take - read: determinism.
> you can also write cache-aligned arrays of structs in Go if you want to
Wasn't this thread about why people don't use GC, not about go? I don't remember.
If you're using an object pool, you're dodging garbage collection, as you don't need to deallocate from that pool, you could just maintain a free-list.
> you can allocate a slab and pull from it if you want to. the existence of a GC doesn't preclude these possibilities
To take it further, you could just allocate one large chunk of memory from a garbage collected allocator and use a custom allocator - you can do this with any language. But you're not using the GC then.
I guess you use every feature (insert any language here) offers for every program you write with it?
The answer to your question is probably: because they like the language, are productive in it, know the libraries and the feature can be turned off so it's an option.
> If you have a garbage collector that runs in 200us
The problem is GCs for popular languages are nowhere near this good. People will claim their GC runs in 200us, but it's misleading.
For example, they'll say they have a 200us "stop the world" time, but then individual threads can still be blocked for 10ms+. Or they'll quote an average or median GC time, when what matters is the 99.9th percentile time. If you run GC at every 120 Hz frame then you hit the 99.9th percentile time every minute.
Finally, even if your GC runs in parallel and doesn't block your game threads it still takes an unpredictable amount of CPU time and memory bandwidth while it's running, and can have other costs like write barriers.
Benchmarking a full sweep with 0 objects to free in Julia:
julia> @benchmark GC.gc()
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 64.959 ms (100.00% GC)
median time: 66.848 ms (100.00% GC)
mean time: 67.062 ms (100.00% GC)
maximum time: 73.149 ms (100.00% GC)
--------------
samples: 75
evals/sample: 1
Julia's not a language normally used for real time programs (and it is common to work around the GC / avoid allocating), but it is the language I'm most familiar with.
Julia's GC is generational; relatively few sweeps will be full. But seeing that 65ms -- more than 300 times slower than 200us -- makes me wonder.
Yep. (You know this, but) just as another data point, an incremental pass takes more like 75 microseconds, and a 'lightly' allocating program probably won't trigger a full sweep (no guarantees though).
these aren't theoretical numbers, they're the numbers that people are hitting in production. See this thread wrt gc pause times on a production service at twitter https://twitter.com/brianhatfield/status/804355831080751104 also referenced here, which talks at length about gc pause time distributions and pause times at the 99.99th percentile https://blog.golang.org/ismmkeynote
> they'll say they have a 200us "stop the world" time, but then individual threads can still be blocked for 10ms+
As far as I know this is still true of the Go GC. Write barriers are also there and impact performance vs. a fixed size arena allocator that games often use that has basically zero cost.
It's also important to consider how often the GC runs. GC that runs in 200us but does so ten times within a frame deadline might as well be a GC that runs in 2ms. Then there are issues of contention, cache thrashing, GC-associated data structure overhead, etc. The impact of GC is a lot more than how long one pass takes, and focusing on that ignores many other reasons why many kinds of systems might avoid GC.
>The reality is you still have to free resources, so it's not like the garbage collector is doing work that doesn't need to be done.
Does it? In most game I expect resource management to be fairly straightforward, allocation and freeing of resources will mostly be tied to game events which already require explicit code. If you already have code for "this enemy is outside the area and disappears" is it really that much work to add "oh and by the way you might also free the associated resources while you're at it". I don't need a GC thread checking at random intervals "hey let's iterate over an arbitrary portion of memory for an arbitrary amount of time to see if stuff needs dropping yo!".
I realize that I'm quite biased though because I'm fairly staunchly in the "garbage collection is a bad idea that causes more problems than it solves" camp. It's a fairly extremist point of view and probably not the most pragmatic stance.
One place where it might not be quite as trivial would be for instance resource caching in the graphic pipeline. Figuring out when you don't need a certain texture or model anymore. But that often involves APIs such as OpenGL which won't be directly handled by the GC anyway, so you'll still need explicit code to manage that.
That being said I'd still chose (a reasonable subset of) C++ over C for game programming, if only to have access to ergonomic generic containers and RAII.
I write quite a lot of C. When I do I often miss generics, type inference, an alternative to antiquated header files and a few other things. I never miss garbage collection though (because it's a bad idea that causes more problems than it solves).
It's NOT "a bad idea that causes more problems than it solves", but still may be a bad idea for the (games, real-time, etc) industry.
GC means not only that memory management is simpler, it means that it goes away for most of the programs out there.
Most programmers I know aren't writing code that run Twitter-like servers or avionics, nor are they programming the next Doom game. These people are writing apps and doing back end coding for some big company where real-time isn't an issue and the focus is on getting code out fast, with good average quality and "cheap" labor.
In this case, having a high level language/runtime that doesn't requirs a programmer that can reason about allocations is key.
I can't even begin to tell the kind of codebase that I have seen. Two years ago I was working on a C/C++ legacy system that was thousands of lines of codes and almost every file had memory leaks (that cppcheck could find itself, mind you). Some of them where caused by delivery deadlines, most of them where caused by unskilled employees.
(oh, in case you're wondering: all those leaks where solved by having ten times the hardware and a scheduled restart of the servers)
> In most game I expect resource management to be fairly straightforward, allocation and freeing of resources will mostly be tied to game events which already require explicit code.
Even if you are perfect, you will still fragment over time. At some point, you must move a dynamically allocated resource and incur all the complexity that goes with that--effectively ad hoc garbage collection.
There are only two ways around this:
1) You have the ability to do something like a "Loading" screen where you can simply blow away all the previous resources and re-create new ones.
2) Statically allocate all memory up front and never change it.
I think your (because it's a bad idea that causes more problems than it solves) needs qualification via "in some cases". It certainly is handy and well used in other applications and speeds up development time as well as prevents various developer bugs.
Unity uses/used the standard boehm garbage collector [1] for over a decade, and it has been notorious for causing GC lag in games produced from that engine, noticeable in occasional sudden spikes of dropped framerate while the GC does a sweep at a layer of abstraction higher than Unity game developers can control directly.
That Unity was using that ancient collector was the main complaint. Switching to an incremental GC is long overdue.
As for pooling objects, you'd go to those "extreme measures" as a matter of course in any other language as well. You wouldn't want to alloc and free every frame no matter the language.
Yes. the boehm collector is the root of the problem for Unity's runtime. It's just the verbatim code from that repository in fact. But in other languages, such as C with standard libraries, you can still do common things like comparing stings and calling printf() without inadvertently triggering a dreaded GC sweep. Not so in Unity's C#.
And allocating memory is fine during runtime when you are in control of the allocation and the cleanup, whereas in Unity, the sweeps are fully out of your control, expensive, and will just sometimes happen in the middle of the action
That API is new and was just added to 2018.3 in December, around the same time they started previewing the incremental garbage collector and promoting ECS.
From Unity:
> Being able to control the garbage collector (GC) in some way has been requested by many customers. Until now there has been no way to avoid CPU spikes when the GC decided to run. This API allows you disable the GC and avoid the CPU spikes, but at the cost of managing memory more carefully yourself.
It only took them 14 years and much hand wringing from both players and developers to address :)
A similar thing is going on with their nested scene hierarchy troubles, also releasing in 2018.3 with their overhaul to the prefab system, to sort of support what they call "prefabs" having different "prefabs" in their hierarchy without completely obliterating the children. What they have now is not ideal, but they're working on it.
Prior to that, if you made. say, a wheel prefab and a car prefab, as soon as you put the wheels into your car prefab, they lost all relation to their being a wheel, such that if you updated the wheel prefab later, the car would still just have whatever you had put into the car hierarchy originally, which naturally has been the source of endless headaches and hacky workarounds for many developers.
All true. However, even before you could disable the GC, you could ignore it as long as you weren't allocating and hit framerate. Unity isn't magic but the way people talk it's like Unity games don't exist.
But things like this would happen, even from acclaimed developers, giving the engine a bad reputation among players:
> The frame-rate difficulties found in version 1.01 are further compounded by an issue common with many Unity titles - stuttering and hitching. In Firewatch, this is often caused by the auto-save feature, which can be disabled, but there are plenty of other instances where it pops up on its own while drawing in new assets. When combined with the inconsistent frame-rate, the game can start to feel rather jerky at times.
That studio is part of Valve now!
> Games built in Unity have a long history of suffering from performance issues. Unstable frame-rates, loading issues, hitching, and more plague a huge range of titles. Console games are most often impacted but PC games can often suffer as well. Games such as Galak-Z, Roundabout, The Adventures of Pip, and more operate with an inherent stutter that results in scrolling motion that feels less fluid than it should. In other cases, games such as Grow Home, Oddworld: New 'n' Tasty on PS4, and The Last Tinker operate at highly variable levels of performance that can impact playability. It's reached a point where Unity games which do run well on consoles, such as Ori and the Blind Forest or Counter Spy, are a rare breed.
If you continue reading that article, they go on to say Unity is not to blame...
There's a lot of reasons shipping Unity games is hard but the GC and C# are not among them or at least much lower than, say, dealing with how many aspects of the engine will start to run terribly as soon as an artist checks a random checkbox.
> In its current iteration, console developers familiar with the tech tell us that the engine struggles with proper threading, which is very important on a multi-core platform like PlayStation 4.
> This refers to the engine's ability to exploit multiple streams of instructions simultaneously. Given the relative lack of power in each of the PS4's CPU cores, this is crucial to obtaining smooth performance. We understand that there are also issues with garbage collection, which is responsible for moving data into and out of memory - something that can also lead to stuttering. When your game concept starts to increase in complexity the things Unity handles automatically may not be sufficient when resources are limited.
Almost a decade ago I was writing a desktop toolkit in Lua 5.1 and gdk/cairo bindings. Let’s say for fun, because it never seen the light in planned business. But it had animations and geometry dynamics (all soft, no hw accel). While GC seems to be fast and data/widget count was small, it suddenly froze every dozen of seconds for a substantial amount of time. First thing I tried was to trigger a [partial] collection after every event, but what happened was (I believe) incremental^ steps still accumulated into one big stop. Also I followed all critical paths and made allocations static-y. It got better, but never resolved completely. I didn’t investigate it further, and my hw was far from bleeding edge, thus no big conclusion. But since then I’ll think twice before doing something frametime-critical with GC. Each specific implementation requires a handful of wild tests at least before considering as an option in my book.
As others probably already mentioned, worst side of gc is that it is unpredictable and hard to be forced in a way that matches your specific pattern. With manual/raii mm you can make pauses predictable and non-accumulating collection debt and fit before “retrace” perfectly. Also simply relying on automatic storage in time critical paths is usually “can not” in gc envs.
^ if any, I can’t recall if 5.1 actually implemented true incremental gc back then
This is simply not the case. Its still just code after all. The problem was you were fighting the GC but that's just the symptom. The clear problem was leaking something every frame. With all the tooling these days its pretty easy to see exactly what is getting allocated and garbage collected so you know where to focus your efforts.
I started with C actually, but good ui is hard, and I decided to use Lua for a reason of not writing it all in C and hand-optimized style. I was forced to write ugly non-allocating code anyway, which defeated that idea completely. To focus your efforts, there must be a reason for these efforts in the first place.
But you must understand that lots of games are able to make this work. Call of Duty, for example, uses Lua for its UI scripting and is able to hit framerate. WoW uses Lua as well. Its not impossible or even impractical. There's plenty of reason to use a higher level language even if your core loops are hand optimized.
>> The reality is you still have to free resources,
Not exactly. Here is how the early PC 3D games I worked on did that: They would have a fixed size data buffer initialized for each particular thing you needed a lot of, such as physics info, polygons, path data, in sort of a ring buffer. A game object would have a pointer to each segment of that data it used. If you removed a game object you would mark the segment the game object pointed to as unused. When a new object was created you would just have a manager that would return a pointer to a segment from the buffer that was dirty that the new object would overwrite with data. Memory was initialized at load and remained constant.
One problem with doing things like that is that you would have fixed pool. So there were like 256 possible projectiles in Battlezone(1998) in the world at any time and if something fired 257th an old one just ceased to exist. Particles systems worked that way as well.
What was good about that was that you could perform certain calculations relatively fast because all the data was the same size and inline, so it was easy to optimize. I worked on a recent game in C# and the path finding was actually kind of slow even though the processor the game ran on was probably like 100 times (or more) faster. I understand there are ways to get C# code to create and search through a big data structure as fast as the programmers had to do it in C in the 90's. However it would probably involve creating your own structures rather than using standard libraries, so no one did it like that.
I think that, the thing I know for sure was there was 1,024 total physics objects, which was tanks and pilots active in the world at any time. So if you built a bunch of APCs and launched them all at a target at the same time, at some point you wouldn't be able to spawn soldiers. No one seemed to mind because in those days the bar was lower.
Not a game developer, but I used to write UI addons for World of Warcraft. WoW allows you to customize your UI heavily with these Lua plugins ("addons") and Lua is garbage collected. It's a reasonable incremental GC so it shouldn't be too bad in theory.
But in practice it can be horrible. You end up writing all kinds of weird code just to avoid allocations in certain situations. And yeah, GC pauses due to having lots of addons is definitely very noticable for players.
Also leads to fun witch hunts on addons using "too much" memory, people consider a few megabytes a lot because they confuse high memory usage with high allocation rate... Our stuff started out as "lightweight" but it grew over the years. We are probably at over 5 MB of memory with Deadly Boss Mods (many WoW players can attest that it's certainly not considered lightweight nowadays, but I did try to keep it low back then).
But I think we still do a reasonable job at avoiding allocations and recycling objects...
The point is: I had to spent a lot of time thinking about memory allocations and avoiding them in a system that promised me to handle all that stuff for me. Frame drops are very noticable.
But there are languages with better GCs than Lua out there...
I’ve shipped C++ games and Unity games. I have never spent more time managing memory than in C#. You have to jump through twisted, painful hoops to avoid the GC.
Memory management is ultimately far simpler and easier in C++ than in C#.
It could be worse. Once upon a time a common "trick" was to avoid using foreach loops because they generated garbage. LINQ also generated garbage. I'm not sure if the .net 4.5 upgrade fixed that one.
Ultimately you wind up doing the exact same thing in C# that you'd do in C++. Aggressive pooling, pre-allocation, etc. A handful of Unity constructs generate garbage and can't be avoided. I believe enable/disabling a Unity animator component, which you'd do when pooling, is one such example.
It's all just a little extra painful because you to spend all this time avoiding something the language is built around. Trying to hit 0 GC is annoying.
It also means, somewhat ironically, is that GC makes the cost of allocation significantly higher than a C++ allocation. In C++ your going to pay a small price the moment you allocate. But you know what? That's fine! Allocate some memory, free some memory. It ain't free, but it is affordable.
In Unity generating garbage means at some random point in the future you are going to hitch and, most likely, miss a frame. And that's if you're lucky! If you're unlucky you may miss two frames.
> I love this opinion from games programmers because they never qualify it and talk about what their latency budgets are and what they do in lieu of a garbage collector.
Bro, game devs talk about this non-stop. There are probably 1000 GDC talks about memory management.
Game devs don't spell the fine details because they are generally talking to other game devs and there is assumed knowledge. Everyone in games knows about memory management and frame budgets.
> If you have a garbage collector that runs in 200us, you could run a GC on every single frame and use less than 3% of your frame budget on the GC pause.
And if pigs could fly the world would be different. ;)
Go has a super fast GC now. It had a trash GC (teehee) for many many years. But Go is not used for game dev client front-ends. If C# / Unity had an ultra fast GC that used only 3% of a frame budget that would be interesting. But Unity has a trash GC that can easily take 10+ milliseconds. (Their incremental GC is still experimental.) It's a problem that literally every Unity dev has to spend considerable resources to manage.
For 50+ years GC has been a poor choice for game dev. Maybe at some point that will change! The status quo is that GC sucks for games. The onus is on GC to prove that it doesn't suck.
Unity is frustrating because the regular C# CLR (and Mono) have a much better generational GC with fast Gen 0 and Gen 1 collections. Unity's crappy GC is always slow.
For games generally you want zero allocations or frees during gameplay. For a GC'd language you want to avoid the collector running during gameplay.
Having done both they are remarkably similar. Lots of pooling and pre-allocation. Most GC based languages are a bit harder both because they tend not to give you a chunk of memory you can do whatever with and require a lot of knowledge about which parts will allocate behind your back. It's also harder to achieve because you typically get no say when the collector will run.
There are loads of commercial games that pay no attention to this though written in both kinds of language.
Here are some common alternatives I've seen and used:
1) Don't allocate or free resources during gameplay. Push it to load time instead. This works for things like assets that don't change much.
2) Use an arena that you reset regularly. This works well for scratch data in things like job systems or renderers.
3) Pick a fixed maximum number of entities to handle, based on hardware capacity, and use an object pool. This works well for things that actually do get created and destroyed during gameplay, on a user-visible timescale.
Together, these get you really far without any latency budget going toward freeing resources. And there is always something else you could put that budget toward instead, which is why any amount of GC is often seen as a waste.
I used to work on realtime graphics code which was used in flight simulators for the FAA and USAF, then moved into the games industry and led the design of a couple of game engines, to give you an idea where I'm coming from. The military grade flight sims actually had contract requirements like, "you may never miss a single frame in a week of usage", etc, so solving this problem was critical.
When working on code such as game engines, it's not simply a matter of how long something takes to do, but also when it happens and how predictable it is. If I knew that GC takes 1ms, I could schedule it just after I call swapBuffers and before I begin processing data for the next frame, which isn't a latency critical portion.
The problem is that GC is unpredictable because the mark and sweep can take a long time, and this will prevent you from meeting your 60fps requirement.
In practice, we hardly ever do any kind of GC for games because we don't really need to. We use block memory allocators like Doug Lea's malloc (dlmalloc) in fixed memory pools if we must, but generally, you keep separate regions for things like object descriptors, textures, vertex arrays, etc. There's a ton of data to manage which can't be interleaved together in a generic allocator, so once you've gone that deep, there's really no point in using system malloc.
Malloc itself isn't a problem either, it's quick. It's adding virtual space via sbrk() and friends which can pause you, so we don't. On consoles, we have a fixed memory footprint, and on a bigger system, we pre-allocate buffers which we will never grow, and that's our entire footprint. We then chunk it up into sections for different purposes, short lived objects, long lived objects, etc. Frequently, we never even deallocate objects one by one. A common tactic is to have a per-frame scratch buffer, and once you're done rendering a frame, you simply consider that memory free - boom, you've just freed thousands of things without doing anything.
There are many things you can do instead of generic GC which are far better for games.
I disagree with the author of the original article about C++. C++ is as complex as you want to make it, you don't have to use all the rope it hands you to hang yourself. However, having the std library with data structures, smart pointers, the ability to do RAII, is invaluable to me. Smart usage of std::ref gets you automatic GC, what you don't have is cycle detection, so you take care never to have cycles by using weak pointers where necessary, and you have all the behavior of auto-GC without stop the world.
I second that. I always find the attitude of “C++ is bad so I’m going to stick to C” really bizarre. You can use C++ as a better C.
- use type inference and references instead of pointers. Writing C style code with these features makes it more readable.
- Don’t like OO programming. Stick to struct with all members public. It’s going to be a lot better than doing the same thing in C and you shall not have void* casts.
- use exceptions instead of return code for errors, so that your code is not peppered with if statements at every line you call. With an exception you’ll get additional info about crashes in dumps.
I wrote for an embedded system where the C++ standard library was not available, in a previous life. I ended up writing my code in C++ and “re-inventing” a couple of useful classes like std::string and std::vector. For the most part my code was very C like...
> use exceptions instead of return code for errors
I happen to have the exact opposite opinion. Exception handling tends to feel too "magical" (read: non-deterministic, hard to behaviorally predict, etc.) relative to just returning an error code.
Exception handling doesn't work very well. It requires exception frames on the stack, and those go missing sometimes, leading to leaked exceptions and unexpected crashes. For example, if you pass a C++ callback function into a C callback, say in libcurl, the C library doesn't have exception handler frames in the stack, so exceptions are lost. If your C++ code throws from within that C callback, any catch above the C language boundary won't work. This is just one of countless gotchas. Don't use exceptions.
Otherwise, exceptions make code simpler, cleaner, and more reliable. They will never "go missing" unless you do something to make them go missing. Code that does those things is bad code. Don't write bad code. Do use exceptions.
Well, of course, that's how you fix such a bug, however, when using third party libraries and other people's code, you don't necessarily know that this is happening until you have missing exceptions.
In my opinion, exceptions also complicate code by taking error handling out of the scope of the code you are writing, since you can't know whether anything you call throws, so you may not catch, and some higher level code will catch an exception, where it's lost context on how to handle it. If exceptions work well in a language; Python, Java, by all means use them, but in C++, they've caused too many problems for me to continue doing so. Even Google bans them in their C++ style guide.
If you don't know what functions you are passing to third-party libraries, you have a much bigger problem than any language can help you with.
Google's proscription on exceptions is purely historical. Once they had a lot of exception-unsafe code, it was too late to change. Now they spend 15-20% of their CPU cycles working around not being able to use RAII.
When you write modern software, most of your executable ends up being third party code for commonly done things, it's both wonderful since you can move fast, and a curse, because you inherit others' bugs.
My example with libcurl was very simple, but in real systems, things are less clean. You may be using a network tool that hides libcurl from you, and your code is being passed into a callback without you even knowing it until it causes a problem. Other places where C++ exceptions fail would be when you dlopen() a module, or one of your binary C++ dependencies is compiled without exception handlers. The code will compile just fine, but exceptions will be lost, there's not so much as a linker warning.
Google uses RAII just fine, it's just you have to be careful with constructors/desctrucors since they can't return an error. There's no way it burns 15-20% CPU not using RAII - where are you getting these numbers? I used to work there, in C++ primarily, so I'm well familiar with their code base.
Instead you call a constructor that sets a dumb default state, and then another, "init()" function, that sets the state to something else, and returns a status result. But first it has to check if it is already init'd, and maybe deinit() first, if so, or maybe fail. Then you check the result of init(), and handle whatever that is, and return some failure. Otherwise, you do some work, and then call deinit(), which has to check if it is init'd, and then clean up.
I knew someone else who worked at Google. 15-20% was his estimate. Bjarne ran experiments and found overheads approaching that.
Unless you know the details of how every function (and every function that function calls, and so on) handles errors, the easiest way to not pass functions to libcurl that throw is to not write those functions in a programming language with exceptions :)
> The reality is you still have to free resources, so it's not like the garbage collector is doing work that doesn't need to be done.
As others have pointed out, this isn't true for many video games. People write things so they're allocated upfront as much as possible and reuse the memory. A lot of old games had fixed addresses for everything as they used all available memory (this allowed for things like Gameshark cheats).
But there's a secondary issue. A GC imposes costs for the memory you never free. It doesn't know the data will never be freed, so it'll periodically check to see if the data is still reachable. Some applications have work arounds for this like using array offsets instead of pointers/references (fewer pointers to follow).
The last time I encountered GC issues in gamedev, we were seeing 100ms GC pauses (that's 6 dropped frames at 60fps!) every 30 seconds or so. We had poor insight into why, and what we could glean of "why" was that it was a widespread problem with how our designers were prototyping stuff in JS (more specifically, they were writing it as regular JS, instead of jumping through hoops to try and relieve GC pressure through pooling objects) with no clear single cause to blame.
Other issues I've seen include nondeterministic GC sometimes failling to garbage collect one level before we loaded another, OOMing the game. Have you ever forced a GC three times in a row to try and ensure you're not doubling your memory use on a memory constrained platform? I have. (This was exacerbated by cycles between the GCed objects and traditionally refcounted objects - you'd GC, which would run finalizers that would decref, which in turn would unroot some GCed objects allowing them to be collected, which in turn run more finalizers, which would decref more objects, ...)
The last time I encountered a similar (de)alloc spike in a non-GC gamedev system was much longer ago, and it was a particular gameplay system freeing a ton of graphics resources in a single frame, when doing something akin to a scene transition or reskinning of the world - which was easily identified with standard profiling tools, and easily fixed in under a day's work by simply amortizing the cost of freeing the resources over a few frames by delaying deallocs with a queue. More commonly there were memory leaks from refcounted pointer cycles, but those also generally pretty easily identified and fixed with standard memory leak detection tools.
The problem isn't GC per se. It's that most/all off-the-shelf language-intrinsic GCs are opaque, unhookable, uncustomizable, unreplacable black boxes which everything is shoved into. malloc/free? Easily replaced, often hookable. C++'s new/delete? Overloadable, easily replaced, you have the tools to invoke ctors/dtors yourself from your own allocators. Localized GC for your lock-free containers ala crossbeam_epoch? Sure, perfectly fine. Graphics resource managers often end up becoming something similar to GCed systems on steroids, carefully managing when resources are created and freed to avoid latency spikes or collecting things about to be used again soon.
But the GCs built into actual languages? Almost always a pain in the ass.
Thing is, I've helped ship my share of GCs in games, and they're not always a problem.
UI solutions often/usually use some kind of GCed language for scripting. But the scale and scope of what UIs are dealing with are small enough that we don't see 100ms GC spikes.
Our tools and editors leverage GCed languages a lot - python, C#, lua, you name it - and as long as their logic isn't spilling into the core per-frame per-entity loops, the result is usually tollerable if not outright perfectly fine. We can afford enough RAM for our dev machines that some excess uncollected data isn't a problem.
And with the right devs you can ship a Unity title without GC related hitching. https://docs.unity3d.com/Manual/UnderstandingAutomaticMemory... references GC pauses in the 5-7ms range for heap sizes in the 200KB-1MB range for the iPhone 3 [1]. That's monstrously expensive - 1/3rd of my frame budget in one go at 60fps, when I frequently go after spikes as small as 1ms for optimization when they cause me to miss vsync - but possibly managable, especially if the game is more GPU-bound than CPU-bound. It certainly helps that Unity actually has some decent tools for figuring out what's going on with your GC, and that C# has value types you can use to reduce GC pressure for bulk data much more easily.
[1] Okay, these numbers are pretty clearly well out of date if we're talking about the iPhone 3, so take those numbers with a giant grain of salt, but at the same time they sound waaay more accurate than the <1ms for 18GB numbers I'm hearing elsewhere in the thread, based on more recent personal experience.
Right, well-contained GC in non-critical threads is not a problem. Once you get it trying to scan all of memory, your caches get corrupted, and only global measurements can tell you what the performance impact really is.
GC enthusiasts always lie about their performance impact, almost always unwittingly, because their measurements only tell them about time actually spent by the garbage collector, and not about its impact on the rest of the system. But their inability to measure actual impact should not inspire confidence in their numbers.
One of the key other things that surprisingly hasn't been mentioned is it's super important to control memory layout.
GC pause times are relevant but not critical. What's critical is that I can lay out my objects to maximize locality, prefetching, and SIMD compatibility. Look up Array of Structs vs Struct of Arrays for discussions on aspects of this.
This is not strictly incompatible with a GC in theory, however it's common that in GC'd languages this is either very difficult or borderline impossible. The JVM's lack of value types, for example, makes it pretty much game over. Combining a GC with good cache locality and SoA layout is possible, but it doesn't really exist, either. Unity's HPC# is probably the closest, but they banned GC allocations to do it.
You somewhat explained exactly why a garbage collector can't work. Just because you need to free up memory, doesn't mean a garbage collector is the only solution. There's a big difference over having control of when to release the memory, or having no control of that. For instance, if you know that there are no user inputs or any kind of interactive things going on, you can afford to take a couple millisecond hit freeing memory.
By game programmers you probably mean AAA console game programmers. I was once one, and I know that those games have to account for every byte of memory. Garbage collection has not only cpu overhead which can be unpredictable, it also requires memory to potentially sit around unused waiting for the next cycle. Your question is what is our budget for gc in terms of cpu work and memory overhead, and the answer is zero for both.
> The reality is you still have to free resources, so it's not like the garbage collector is doing work that doesn't need to be done.
The garbage collector also needs to track resources, which is an additional cost over just freeing them. You have little control over how the memory is allocated, which is an additional cost over a design that intelligently uses different allocation strategies for different types of resources. Then, even if you can control when the garbage collector is invoked, you have little control over what actually gets freed. What good are those 200µs if the stuff eating up memory isn't actually getting freed fast enough?
Maybe people often overestimate their performance needs. A garbage collector may be fast enough for more purposes than anticipated. Even then, managing memory intelligently may seem like a small price to pay compared to the prospect of eventually fighting a garbage collector to get out of a memory bottleneck.
I just want to say that no, you don’t need to free the resources. It's very possible to use a fix amount of heap memory during the whole lifecycle of your game. In fact I think that’s what people should aim for in 90% of the cases.
Here's the easy question: can you do manual GC in a GC'd environment? Yes, of course you can.
Here's the harder question: how hard is it to do manual GC in a GC'd environment vs. a non-GC'd environment?
"Environment" is the key word here. Because if you're writing in a GC'd environment, there's a good chance that existing code - third party, first party, wherever it comes from - assumes that it can heap allocate objects and discard them without too much thought.
So for small scale optimizations where you own it all, it can work out fine. But if that optimization needs to bust through an abstraction layer, all of a sudden the accounting structure of the whole program has changed, and the optimization has turned into a major surgical operation.
This was almost exactly my experience when I wrote a network simulator in Go and tried to improve the performance. The gc pauses reached tens of seconds haha, I made a lot of garbage.
I'm not completely sure if this is true but I think that doing things in an "OO" style where for example, every different event was it's own type which satisfies the Event interface basically means that different events can't occupy an array together and that I think that each one may hold a pointer to some heap allocated memory, so you really can't optimise this away without ripping up the entire program.
Rather than do so, I ended up running my Sim on a server with >100 cores, it was single threaded but they would all spin up and chomp the GC, a beautiful sight.
Another factor is just the general lack of transparency or knowability of where and how objects occupy memory in these languages.
If memory management is likely to be a concern it is absolutely much easier in an environment where it is prioritized than one where it is ignored.
It serves the huge purpose of doing 100% of the tracking work. Whats easier? Manually tracking all of your allocations and frees as well as worrying about double frees and pointer ownership or calling GC.Collect() during a load screen?
What's easier? Knowing that you can use all your memory again after you've explicitly freed the resources you used, or knowing that you can use all your memory again after a call to GC.Collect() does god-knows-what?
I had the very same thought. It is unsubstantiated. But if you consider how a game actually uses memory, it is really not suited for your run-of-the-mill GC: Most assets are allocated once per level/scene/whatever and have a pretty well-defined lifetime. What's left happily lives on the stack frame. There is may some room for reference counting of some structures (think multiplayer et. al. where the engine does not have full control of the flow). But in general, a GC can provide very little here, at quite some cost.
In particular I know that Go's GC is optimized for very low latency (rather than throughput), and is not a stop the world GC. So I'm wondering why it doesn't work for games? Are the pauses still too high for games, or is he missing something? Benchmarks or concrete results in Go would be great here.
Edit: specifics from https://blog.golang.org/ismmkeynote - Go hugely improved GC latency from 300ms before version 1.5, down to 30ms, then again down to well under 1ms but usually 100-200us for the stop the world pause. So I guess it is stop the world but the pauses seem ridiculously small to me. They guarantee less than 500 microseconds "or report a but", which seems more than fast enough for game framerates (16ms for 60Hz frames). Am I missing something?
The thing with games compared to regular services is that they start with the bar of rigor a bit higher than normal in all respects.
GCs really let you trivially not care about a LOT of things... Until you need to care. Things like where and when you allocate, the memory complexity of a function call, etc. With games you need to care about all that stuff so much sooner.
Once you're taking the time to count/track allocations anyway you might as well just do it manually. It just codifies something your thinking about anyway.
Short stop the world is well and good, but some of the performance impact is pushed to the mutator via write barriers. That can be another can of worms.
maybe this is an ignorant question, but why do people always cite GC latencies in seconds? is there an implied reference machine where the latency is 300ms? I would expect it to vary a lot in the wild based on cpu frequency and cache/memory latency. is there some reason why this doesn't matter as much as I think it does?
People understand seconds, and any other measurement would require specifying a lot of computer-specific stuff, and if you're going to do that, then you might as well fully specify the workload, too, to answer those questions before they come.
It isn't meant to be a precise answer, it's meant to put the GC performance broadly in context.
People don’t write games in GC languages, so GC language developers don’t optimize for games, so people don’t write games in GC languages. It’s just a vicious cycle. It never breaks because for big projects there is too much financial risk.
Also, because everybody who tries optimizing a GC language for games fails, badly. And, GC language developers know that if they promote their language for games, any game developers (temporarily) taken in will hate them forever, and bad-mouth their language.
Languages not used for anything anyone cares much about don't attract much bad press. There are reasons why games are written the way they are. It is not masochism.
> I love this opinion from games programmers because they never qualify it and talk about what their latency budgets are and what they do in lieu of a garbage collector.
You don't garbage collect. Aaah game I worked on had p much every object type pre allocated at boot. Things ran until you hit a loading point. At which point you wrote new object data over the existing objects. This isn't garbage collection technically but it's the same thing. Ever wonder why some games have shit load times? Cause that's when the equivalent of gc is happening. Except like everything else in games it's coded by the engine team instead of the language implementer and so is hooked into and looks different. The game knows when it can go to shit and all of the optimizations have been done pre launch usually (if it's performant).
Basically think about how you can either build a function as recursive and leverage the stack that is built into the programming language or you can write the exact same algorithm in a for loop with a stack object and manipulate the flow of the recursion yourself. they're essentially the exact same thing however one is in complete control of the application writer and the other one is in control of the language. if you use the for lube you can actually say I'm going to run out of memory before you hit the stack limit and actually do something smart it's much harder to do this in a programming language with a built-in recursion. for instance you might approach the stacked limits in the for loop and just returned the current answer.
essentially this analogy goes for the difference between garbage collection in many games and garbage collection in garbage collected languages
The problem with GC is that releasing memory is not deterministic. With a tracing GC, you don't know exactly when a pause will happen, and you don't know how long it will take.
Static lifetime management, by contrast, gives the same benefits to memory errors that static typing does to type-mismatch errors: you know, at compile time, how long an object will live and when it will be released. And armed with this knowledge, you can then more effectively profile and optimize.
There are, however, much better ways to go about it than C provides. RAII in C++ and Rust and even ARC in the iOS runtimes allow you to get most of the benefits of automatic memory management while still providing strict, deterministic lifetime guarantees.
> The reality is you still have to free resources, so it's not like the garbage collector is doing work that doesn't need to be done.
This is quite blatantly false. The reality is that every single time your GC pass traverses an object but doesn't free it, it's wasting time doing work that didn't need to be done.
Is the claim that 200us would be the worst case time for the GC to complete its work?
Is this worst case measurement one that the language itself guarantees across all platforms targeted by the game?
If you haven't measured the worst case times, and the system you are using wasn't designed by measuring worst case times, then we're not yet speaking the same language.
You're right, this is a good discussion! Unity introduced an incremental GC that splits the work over several frames, essentially performing an entire sweep "for free"
I have mad respect for all using low level C / Assembly / Rust in gamedev. But I have a hard time recommending anything other than C# / Unity for secondary school 14-15 year olds just starting their journey. The wealth of resources, tutorials and community online is astounding. And as an entrypoint into VR / AR it's difficult to top ;)
The two most popular engines (Unity and Unreal) both have a GC for “front end” gameplay code. The trade-offs are real: manual resource management would be incredibly complex for large modern games (think open world, multiplayer), but GC spikes are a common issue in both engines.
The places where you generally don’t want a GC are with low level engine code, like the renderer - code that might execute hundreds or thousands of times a frame, where things like data locality become paramount. GC does you zero favors there.
"What they do instead of GC" - with some languages there is very little need for GC. Sometimes it's just 0, sometimes it's only certain limited kinds of allocations, and those can have scheduled releasing/cleaning/GC-like activity.
You can just use a GC that doesn't collect until you tell it to and just call it during load screens and preallocate all your object pools. Its not that bad.
The trick is using libraries and techniques that only stack allocate or use your pools. These are techniques you'd almost certainly use without a GCed language but somehow people consider using C before using these techniques is a higher level language.
I’m not a game programmer but with RAII for example (I realize this is not a C idiom), it really becomes a non-issue if you have things scoped properly. For everything that doesn’t fit into this box you probably wouldn’t be relying on a GC anyway, the GC may do the final memory reap but scope control is still often manual in memory-managed languages.
That's not entirely true. If you only rely on RAII you may end up with millions of shared pointers, each of which needs to be managed independently and having its own overhead. They may also be independently allocated, which means they will be fragmented in memory. Not so simple.
I think it’s safe to say for the general case that if you have millions of widely-shared smart pointers you are not following RAII principles. At that point you are left with poorly-implemented reference counting, with a ton of synchronization overhead.
If the lifetime of your objects are not bound to any scope, I’d say you’re well out of the realm of any real benefit that RAII would provide over GC. This is a continual problem that arises with a shared ownership model. The specific principle this violates is encapsulation - there is still encapsulation at the level of the smart pointer, but not with its consumers.
Indeed, if you have a noticeable number of shared pointers, you are Doing It Wrong. A few here and there at top level are harmless, but if they have leaked into public interfaces or low-level infrastructure, you might better pitch the whole codebase and start over.
Java has a pauseless GC called Shenandoah, and Zing JVM from Azul systems also has pauseless ZGC (I believe Zing implemented it first). What most people don't realize is that eg. Shenandoah can slow down the allocation rate to keep GC in the time budget.
I think people prefer C and C++ because they can control the memory layout of the app, and specially in console games they can do much more low-level optimisations than using a higher level language. Besides, I'm guessing the pain in C++ doesn't come from memory allocation, you probably follow a well defined ruleset for freeing objects and you know you will be OK.
Well, most game engines tend to have a scripting language of some sort and that's usually GC'd. The reason that works ok is usually if you're embedding a language you can control how much time is spent on GC (and in the language itself). Doing an entire game from scratch in a memory managed language, it's not so much that GC is slow, it's that it lacks adequate control. (I don't think it's a coincidence that a lot of the popular runtimes -- python for instance -- tend to be reference counted with GC mostly reserved for cleaning up cycles.) If you're concerned with performance, it's not so much that GC _can't_ be fast, it's that you really don't want to give up control on that sort of thing because GC spikes are really annoying to fix.
The problem with gc is that it can blow out a frame's budget, and perhaps extend for more than one frame, taking away precious nanosecs, dropping a 60fps down to 15.
> what they do in lieu of a garbage collector.
Use reference counting. If in the rare case you need circular references then you use a weak pointer, or reclassify your structure so that it's not circular.
That said, where and how you manage the heap can be critical. I recall a few places where I've used the stack itself as a temporary (small) heap, which all gets cleaned up when you exit the function, taking up almost zero clocks to do so.
Reference counting is a form of gc though. It even comes with some nondeterminism in the form of release cascades. Usually performs worse than heap analyzers too, due to hitting atomic counters a lot. The lack of heap overhead and simplicity of implementation are the strong points.
Go is honestly probably not a great language for game development for other reasons but the GC could likely clear that goalpost. https://blog.golang.org/ismmkeynote
Err not so fast there. It's quite common in C to allocate an array of objects (not pointers) and reuse them as they expire. Memset is enough to reinitialize them.
And the the main point, lot's of game dev is "C wrapped in C++", game devs tend to rewrite everything for performance and predictability, relying on STL is usually a nope.
You can choose when and how and if to free resources rather than having it be rather opaque with the GC, that is the key.
Though people who make games sometimes overstate the importance of this. Minecraft is arguably the most successful game of all time despite GC pauses. A competitive shooter like CS:Go, however, can't have that.
not sure about other managed runtimes but in Go you can disable the automated GC scheduling and schedule it yourself. The GC runs in sub-millisecond time. https://blog.golang.org/ismmkeynote
the point isn't "Go is good enough", because I don't know if it is because the requirements are not stated particularly clearly. If the GC took zero time, duh, it would work and people could use it, it would save them some error cases and make their programming lives easier. If the GC took 3 seconds, it doesn't work. There exists some latency value under which a GC is fast enough that there's no sane argument for -not- using it. What is that value? That's the question at hand.
> Indeed, and also what about the effect of multiple cores? You could have 7 cores working on game logic and one doing GC.
Note that those independent cores all share common resources. L3 cache & memory bandwidth are both shared, and a GC workload slams both of those resources pretty heavily. So it's still going to have an impact even though it's running on its own core.
not sure that cpu affinity is trivially solveable to avoid mutex contention here, since game entities can often mutate one another when they interact. how do you determine which entities are computed by which cores to minimize the cost of synchronizing the work between the cores?
One strategy might be to correlate CPU affinity with spatial proximity. That is: two entities that are close together are more likely to mutate one another than two entities far apart, so the two close together should be on the same CPU/core/thread and the other should be on a different thread (probably with other entities closer to it).
Even Unreal Engine has garbage collection for UObjects. I love C but I don't think it's necessarily the best language for games programming anymore, I believe C++ is more suited since games fit into the OO paradigm so much better. I am only a hobbyist though so my opinion probably doesn't matter.
EDIT: Unreal Engine is primarily written in C++, and has garbage collection. I'm not salty about the downvote, but I would like to understand why what I have said is apparently incorrect.
You're not wrong about GC becoming more common in games, as you say Unreal has a GC and so does Unity.
Unreal Engine's GC at least is pretty custom. A lot of effort has been put into it to allow developers to control he worst case run time. It has a system that allows incremental sweeps (and multi-threaded parallel sweeping) and will pause its sweeps as it approaches its allotted time limits.
That said, in my experience with Unreal most large projects choose to turn off the per-frame GC checks and manually control it. For example one project I've seen only invoked the GC when the player dies, a level transition happens or once every 10 minutes (as a fallback).
> there are simple ways to avoid shooting yourself in the feet with those
A cursory look at the CVE list for any C software in the wild indicates that no, there are not simple ways to avoid shooting yourself in the feet with manual memory management in C. It's _incredibly hard_ even for "elite" programmers who have a lot of incentive to avoid these problems. The counterpoint is that people are probably going to spend less time looking for ways to maliciously use those problems in your C game than they are in the Linux kernel, but there is plenty of potential for even seasoned C coders to engage in bullet-on-foot violence.
Yeah, I am not talking about vulnerabilities.
I am simply talking about ease of use and avoidance of leaks and crashes.
Writing super secure code is usually not a requirement for gamedev, and I completely agree, this is hard and potentially harder in C than, say, in Rust.
But I've seen some real weird stuff before. ;) Folks using variable-length fields and happily just dumping the contents into eval(), all sorts of awful.
No. Security is a full-stack endeavor. At all levels it is incumbent upon software professionals to build secure and resilient systems, any time they're exposed to a network. The OS should backstop your efforts, not replace them.
I disagree. As a user, I want to be able to run weird software without having it impact the rest of my system. Browsers and mobile OSs get this much more right than desktop OSs.
This does not meaningfully follow. What you "want" is orthogonal to the responsibility of the vendors of software.
The OS should protect where it can. So should software, lest your networked game nuke, say, the parts of your home directory to which it has permissions because without those permissions it can't do something it needs to.
This is just defense-in-depth. It's super basic stuff and HN is literally the only place I see people galaxy-braining about the idea that you have an obligation to not write shit code.
I suspect GP's point is that a networked game should be limited in access to only parts of one's home directory relevant for that game (that is: the game's own save data or configuration files or what have you). It is absolutely the job of the operating system to provide that sort of sandboxing/isolation.
That's not the point. The point is, if you do network I/O, you are processing untrusted packets from the internet. If a carefully crafted set of such packets can trigger, say, a buffer overflow, you're toast.
A first line of defence is authentication. Make sure the packets come from a trusted source (you can MAC each packet for this), so only those trusted sources could possibly trigger the vulnerability.
If the only trusted source is an authoritative server, you might be able to hide that buffer overflow. If your network code is peer to peer however, you have to trust the other players not to run a cheat that takes over your machine… but at least that's not the whole internet.
In fact, generally a memory safety vulnerability will initially manifest itself as a crash before a more advanced targeted exploit can be used against it.
These are game developers though. On the list of requirements and priorities, I'm sure code safety is right beside commenting code and clean code. Not in a disparaging way. But tight deadlines, crazy expectations and insane work hours doesn't leave much time or energy to worry about it.
Not sure that's a good point. The CVE list for almost any programming language can get you to the conclusion that you can shoot yourself in the feet despite not being as tricky as C.
Writing game engines in C++ without using any OOP features, makes perfect sense to me. Other than the author likes C, he failed to show me why his approach is better than yours. I found it very light on details.
Writing game engines in C++ without using destructors is just foolish.
If by OOP you mean virtual functions? Nobody uses that stuff much anymore, except Java refugees. (They use shared pointers, too.) But, anyplace you would use a function pointer in C, it's cleaner with a virtual function, and often faster.
> Writing game engines in C++ without using destructors is just foolish.
Why? My understanding is that in modern game dev you're mostly traversing large arenas of structured memory, and you avoid freeing individual objects like the plague.
Memory is just one kind of resources. Games do tons of I/O, often gigabytes/second. To illustrate, any PC game has a lot of places which call GPU driver like this:
With C++ RAII, you can do something like that instead:
MappedBuffer mapped{ buffer }
// Write memory address pointed by mapped.pData.
// No need to do anything else, the destructor of MappedBuffer will unmap.
Because c++ without OOP still gives you RAII (which is neutered a bit if you don't write your own classes but still), safe pointers, references, lambda, and an immense standard library, to name a few.
OOP is such a nebulous thing. Just because the word 'class' is used doesn't mean something is OOP. Those smart pointers don't use inheritance, they don't implement any interfaces, no virtual methods...
I would call that using a subset of OOP features[1], not avoiding it entirely.
But I guess there's no point in arguing over semantics.
[1] As Wikipedia defines OOP: "a programming paradigm based on the concept of "objects", which can contain data, in the form of fields (often known as attributes or properties), and code, in the form of procedures (often known as methods)." https://en.wikipedia.org/wiki/Object-oriented_programming
It's not classic OOP without inheritance and virtual functions.
But nobody seriously uses virtual functions anymore, except where they would need to use function pointers, and for precisely those uses virtual functions are way better: cleaner, safer, often faster.
Only operator overloading is really important and would cut down the size of code written substantially.
Thats the only real advantage of using C++ vs C.
Everything else oop, function overloading,RAII,etc can be replaced with macros and structs.
I think he is the perfect example of why I don't care what those C (or minimalist subset C++) programmers have to say. When you actually see how he works, how much time he spends debugging his messy code. They are all sooooooo far away from what I would describe as remotely good, clean code. And all so full of themselves too of course.
I liked Handmadehero for this reason, he is so arrogant and certain about how to properly write code, but when you actually see how he codes, what mess he produces and how long he spends live debugging it all the time. He constantly finds bugs in code he wrote earlier, etc. Yet he is so totally unable to find anything remotely wrong about the way he does things, and that there might have been some ideas invented in the last 30 years that could make him more productive.
I have zero respect for those types of programmers. I would quit any job instantly if I ever had to work with guys like this.
I don't know, he seems extremely productive to me. A lot of the time he spends in the debugger is more of him trying to figure out exactly what is happening with an api or memory usage rather than trying to figure out something he coded "wrong".
> [C++] is high performance, and it offers but features I don't want, and at a great complexity cost.
1. Complexity of implementation is not complexity of use. Example: Take std::array vs a plain C array. std::array is a few hundreds of lines of code. But - it's about as inutitive to use; has more convenience features; easier for the compiler to optimize; and doesn't let you should yourself in the foot that easily.
2. If Whiting don't want _any_ of the features of C++ - his software-writing skills are suspect. Now, few people need or want all of C++'s features - but C's are definitely insufficient, especially if you consider both the langauge and some of the standard library (or non-standard common libraries).
3. The complexity you have to tackle with C++ is lower than the complexity of non-standardized often-inconsistent idiosyncratic platforms and libraries for achieving the same things with C (or C + assembly).
So I find Whiting's argument for C over C++ unconvincing. IMHO C++ would be the least-bad language for writing demanding games in.
Also, the C++ committee is very serious about not paying for what you don't use. You can rest assured that there's no unused, hidden C++ bloat that is chipping away at your frame budget.
This is not meant to be a strawman against the complexity cost argument, it's just a related feature of C++ that I thought might be worth explicitly pointing out in case anyone is misinterpreting.
Also, if anyone has a credible dispute against the claim I'd like to hear it.
You are describing 'zero cost abstractions'. Except that they are anything but zero cost. Game developers and low level programmers shy away from them because of their impact in compile and build times, debug build performance and stack trace bloat, increased cognitive load, reduced refactoring ability. Even std::unique_ptr has runtime costs in release builds that a raw pointer does not.
In game development performance is a feature and anything that makes performance and memory usage less deterministic is frowned upon, like a GC. But iteration times are still paramount and things that make compile, load and link times longer and make it more difficult to debug are also frowned upon. Yes, we'd like to have our cake and eat it too :)
No, that's a different (and orthogonal) concept. The point is that in C++ "you don't pay for what you don't use" - whether it's a cheap or an expensive abstraction, if you don't use it then its overhead won't affect your code's performance.
However, many of the C++ abstractions are zero-cost in many common cases.
> Game developers and low level programmers shy away from them
No, they don't. They shy away from abstractions which are too expensive; or whose scope of optimization doesn't fit the game's use; or which don't combine well with out-of-standard-library code used in the game etc.
But game developers use _lots_ of C++ abstractions; and fancy non-C-like C++ features.
Yes, really. It's fine for simple types but for more complex types in more complex situations, you pay the price.
Unique pointers carry around not just the type but also a default deleter, if you provide one. That deleter has to be copied around, checked for nullptr before execution and set to nullptr when ownership changes.
It's pretty stupid to compare a unique_ptr with a deleter that contains state to a pointer - obviously those two things are completely different and the unique_ptr contains way more information.
For C++ < 20 replace `std::make_unique<int>()` by `std::unique_ptr<int>(new int)` (which with all explicit uses of `new` can end up not being safe in some cases (in that case, only if you've not enough memory to allocate an int which should not really be a common occurence...)
For C++ >= 20 you get std::make_unique_default_init which does that properly.
Serious game developers often do not use STL as a matter of habit and principle.
That goes for most of your points about standard libraries. If you are developing code that you expect (or hope) to run on Windows, Linux, Nintendo consoles, phones, PS4's and Xboxes and expect to know the real performance cost of operations, you will be writing all of your own standard libraries anyway. It is very much a problem of "...the complexity of non-standardized often-inconsistent idiosyncratic platforms and libraries for achieving the same things."
> Complexity of implementation is not complexity of use.
And that's where the language complexity comes into play. Game developers who are bootstrapping need to implement and use these pieces. If you are simplifying your life with C++ features, you must know the ins-and-outs and you cannot rely on new language features because you don't have a choice of compiler in most cases.
> I find looking up language features, and quirky 'clever' api's incredibly tiring. The ideal language would be one I can memorize, and then never have to look things up.
This is a fair ask and is definitely more accurate of C than C++. What you might write is longer, more tedious code, but that is its own trade-off.
"...I care about the speed of the compiler. I am not a zen master of focus, and waiting 10+ seconds is wasteful, yes, but more importantly it breaks my flow. I flick over to Twitter and suddenly 5+ minutes are gone."
Some people just like writing code. You can't say that C's features are insufficient, when you can implement the vast majority of C++ features in native C, and all of 'em with tooling. What's wrong with having direct control of only the features you need? What's wrong with code generation? Not to trample on C++, I like it (albeit less than C). I would definitely create C++ if it didn't exist.
> when you can implement the vast majority of C++ features in native C
No, you absolutely cannot, even in principle. C++ is not "C with classes" and some syntactic sugar like it started out. That's not been the case for many years already.
Also, even the features you can implement - you won't; you don't have the person-years for that. You will have to, need to, use libraries. For those you need to compare the libraries available for C and for C++, and C comes up quite short (not just because C libraries are also usable in C++...)
So which feature can you not implement? The only I can think of which you'd have real problems are anonymous functions/lambdas. Most other features I think you can implement. It won't look the same, but it will serve the same purpose.
Destructors. It is the defining feature of the language; everything else in the language extends their reach. They are the key to full exercise of code paths, and thus reliable code.
A programmer who makes full use of destructors is simply a better programmer than one who doesn't.
Well, he does say why he likes C better, so it is useful for others. The problem is that learning how to use C effectively is a lot harder than learning other/modern languages. Therefore, anybody who doesn't know how to use C is probably better of learning one of those other languages.
15 years ago, I spend some time modding with the Quake 3 engine which is written in C too and while I wouldn't categorize myself as 'knowing how to use C effectively' I sympathize with the author's opinion. I totally agree with Go being a revisited C and that C can enable simplicity.
The only part where my opinion is different is about Javascript. Yes, it is a dangerously fast-moving ecosystem, but I think it offers so much value in terms of cross-platform development that I think it is a mistake not to participate. After all, the browser APIs are quite stable and the biggest issue is the constantly changing trend about which libraries are going to be the next big thing.
My experience with Rust is that I have to fight the compiler a lot, but when the program compiles, it works. If it doesn't work, it means there's an error with my file/network paths or I did something in the wrong order, errors which no language can save me from.
Rust also becomes a lot less verbose when you get better at it. The ? operator is especially useful.
It is (or should be) read as slightly hyperbolic, as there isn't to my knowledge a language that actually works that well. I could water down my message by saying "it mostly works", but then I fail to convey the main reason why I posted in the first place.
I could also instead spend a lot of time and words to explain things about Rust's design that I presume anyone who is slightly interested in the language would already know (specifically: lack of null, strict ownership checking, having to explicitly deal with errors), in order to explain that the language succeeds at solving some of the problems it was specifically designed to solve. But then I would just be repeating things that HN readers presumably already know. Instead I can use a shortcut in my communication which is perfectly understandable if you assume minimal intelligence, and take the other person's comment in good faith.
See, by the time we've reached the bottom of this wall of text, anyone who read this far through my intentional rambling has presumably forgotten my initial point: I had a positive experience with the language.
And at the same time, I've heard people say "when it compiles, it works" about much weaker type systems, like Go's. It just seems to mean "this language catches more errors at compile time than the previous language I used."
> It just seems to mean "this language catches more errors at compile time than the previous language I used."
That is a fair interpretation. I've programmed mostly in C# and Java because that's what was required at the time. I also know enough C and C++ to aim at my toes instead of the entire foot. So the comparison is between strongly typed imperative programming languages which are syntactically close to Rust.
Also, I like that Rust isn't OoP, but try to not get baited into that discussion.
From what I've seen if you fight the compiler a lot it means you're trying to write something that very non-trivial memory ownership. Just as RAII is a learned concept, so is understanding memory ownership. Hopefully it will be taught more in school along the lines of RAII and similar concepts.
Ironic, the most complicated thing for me to understand when I started writing rust was borrowing and lifetimes. The rest of the common features of Rust isn't too complicated to understand.
Understanding the concepts is not the issue with me (as of now). It is the hard to read syntax (my personal opinion!). It puts a lot strain on my mind. Same is the case with C++ with extreme use of STL. I have difficulty in reading that, although with clear mind, I will eventually get the code, it is not fun.
Same, I heavily prefer less symbols in my code. I think it might also relate to how I find it much harder to remember long formulas but than remembering the sentence describing the formula. If I could type for example `brw x` instead of `&x` I think I'd like it more - I guess I like it more Python-y?
I'm not sure that rehashing Rust vs. C is particularly interesting at this point. At least not for those who regularly read HN. Maybe in another few years...
"This is just an opinion but, it's also objective truth, you'll be a superior human in everyway to lesser mud-blood human's who never managed to release anything. Everyday they'll continue to toil away in the fields like Russia serfs, while you can skip past them like a wise and jolly Rasputin. (I've released exactly zero games independently, so I am too one of the serfs, with only homemade vodka and sweet dreams of a Bolshevik ruled future to keep me going.)"
I have similar frustrations using C++, but don't feel using C or even Rust would be an improvement, more of "side-grade" due to various tradeoffs those languages have for game development compared to C++.
I am excited about the development of both Zig and Jai since they both are trying to fill what I believe is a much needed role of a C-like language with a few more nice language features like better compile-time code, better compiler error messages, better error handling that doesn't use exceptions, and build scripts written natively in the languages, all while sticking to fast compile times and simplicity.
Games are a decently different domain than others, and there is a lot of valid complaining about C++. It's a known issue, we just need something that's clearly better, which so far I think those two languages are. I've chipped in a few dollars a month to support Zig and have been using it in my free time, but it still very much feels like the version 0.4.0 that it is.
This wait will probably be a few more painful years.
I disagree - I work at a AAA games studio and we're currently building a little project made entirely in C. Not even a hint of C++. This is probably going to end up being just a little research thing in the end, but don't think that no one does this anymore.
Anyone who is interested in secure, plain, well-written C-code should checkout the OpenBSD project. I've just been browsing through the code to brush-up on my C as it's been a few years since I've had to use C in anger, and it's a joy to read.
Whenever I look at BSD code, it reminds me of how we did everything in the '80s. There's an esthetic to stone knives and bearskins (check out "Primitive Technology" series on YouTube) but performance suffers on modern hardware.
Linux is irredeemably complex, and getting moreso all the time. My code nowadays bypasses it wherever possible -- O_DIRECT files, memory-mapped files, kernel-bypass NIC libraries that set up hardware ring buffers mapped into user address space.
> All my solo project games I've been making recently have been written in 'vanilla' C.
The author is talking about solo projects, not big projects where dozens of teams may be collaborating. It is good that he makes the point at the beginning.
> Nobody does this.
This is just not true. There are solo projects developed in Python, Go, even Commodore 64 assembly. When you do something for fun, you can have as much fun with it as you want.
> It would be nice to make things for the web, but it feels like a terrifyingly fast moving enviroment.
The author should look into Emscripten. It is widely used in the games industry that aims for the web.
> C++ ... It is also slow to compile compared to C.
Probably an important point. You need very good practices to make C++ compile fast. Most people don't follow them as adds complexity to the project.
> Haxe feels much more promising than most alternatives.
Haxe is kind of Object-Oriented and like C++. So, I do not see the point after bashing C++'s OOP so badly.
For me, the problem is that it transpiles to another languages. So, when something fails does it in code that you didn't write but the Haxe did. This makes debugging harder that it already is.
> Some people just say screw it, I'll write my own language, the language I want to use. I admire this, and sometimes I toy with the idea of doing the same.
Yep. Something possible for your own fun. Not a good idea when developing commercially. It makes sense for him to choose one language for commercial projects and another, or even create his own, in his spare time.
> It can be made to run on just about anything. Usually this is relatively easy. It is hard to imagine a time when this won't be the case.
This is the big advantage of C. If doesn't run C, probably it just doesn't run at all.
> I absolutely DO NOT mean to say "hey, you should use C too"
For solo projects is a giver that each developer should use the best-suited tools for the task and their abilities and preferences.
I, personally, like C++. I have developed big and small projects in C++ and for me comes natural to write object oriented code. For big projects I see advantatges in being more rigid than C. The extra rigidity makes more difficult that there is unexpected funky stuff happening. So, it is easier to use for teams that maybe situated in different countries. For a solo project, you do not need that rigidity as you know how your own code works.
> > It can be made to run on just about anything. Usually this is relatively easy. It is hard to imagine a time when this won't be the case.
> This is the big advantage of C. If doesn't run C, probably it just doesn't run at all.
Is that really an advantage of C over C++? They have identical runtime requirements and typically share a common compiler (although one of those, MSVC, barely supports C). Is there actually anything that can run C but which cannot run C++?
> > It would be nice to make things for the web, but it feels like a terrifyingly fast moving enviroment.
> The author should look into Emscripten. It is widely used in the games industry that aims for the web.
Emscripten is wonderful. If you are slightly careful, a standard C SDL2 OpenGL application can, with a few small changes, run directly in the browser — any modern browser — with WebGL, and your single codebase can target both just by having different targets in your Makefile. I've done it for my own hobby game project. :D
It is a bit constraining as a target. Only WebGL 1.0 is supported by all major browsers, which limits you to OpenGL ES 2.0. And unfortunately, in my experience OpenGL calls in Emscripten (translated to WebGL) are much slower than native ones. But if you're content with not trying to do a max-fidelity AAA experience, it's manageable. Particularly compelling to me is the possibility of letting someone try out your game immediately on your site with no download, or even jump into a multiplayer game with a friend with just a click on a link.
>On the other hand, it is rapidly getting very complicated
This seems to be the case for almost any "C replacement", and it will be (my prediction, at least) the reason they all fail.
I feel I might be a bit of an outlier in this respect, but I have only ever enjoyed using languages which are small and simple - C, Go, Scheme. The times I've tried Rust, it's been nice to have code that cannot segfault, but I find it such a chore to deal with. Whenever I've read open-source projects in Rust it's always a readability disaster. Also the compiler emitted warnings because functions were not snake case, which I found obnoxious.
I feel zig is now experiencing feature creep now too sadly. Async and await and coroutines are being added to it, because it's the new-ish hotness I guess? Stopped paying attention to the language after that. I was hoping for a more streamlined language.
I was strongly resistant to it when it first came out in C# something like 10 years ago. I thought it was language cruft that was better handled with a library solution, and nothing more.
I was convinced to give it a real try about 4 years ago, and had a serious change of heart after only a week or so. It was just syntactic sugar, but it was syntactic sugar that made concurrent code much easier to write. Not just in terms of making the code more verbose, but in terms of making it easier to do correctly. Because, at least in the way the C# team designed it, it subtly steers you towards doing things in smarter and safer ways, and away from the usual hot mess that is multithreading.
I've heard similarly positive things about goroutines, which seem at first blush to be about the same thing.
Given that Zig is ostensibly about helping people to write performant code more safely, that leaves me thinking that there's an argument to be made that async/await are a good fit for Zig's mission, and not just kitchen sink features.
I’m not sure why but programmers like to reject entire languages instead of just rejecting the features they don’t like. A lot of stuff in C++ is entirely avoidable.
Protocol-oriented programming is extremely useful for games and I like Objective-C for that reason. (Objective-C doesn’t meet his mentioned goal of portability but this is an example where C++ could be used to gain “light” inheritance without having to opt in to the rest of the mess that C++ can be.)
Compilation times even for simple C++ programs are far too long and it's not like you can reject this "feature" unless you basically write C code compiled with C++ compiler. For instance: simple hello world with iostreams takes 280ms to compile vs 40ms for C equivalent with stdio. And it only gets exponentially worse with bigger programs.
> I’m not sure why but programmers like to reject entire languages instead of just rejecting the features they don’t like.
The article mentions that:
>> What I want from a language
>> The strongest thing on my desired, but not required list is simplicity. I find looking up language features, and quirky 'clever' api's incredibly tiring. The ideal language would be one I can memorize, and then never have to look things up. (emphasis mine)
Very interesting point of view. I wish I had projects (like games) that could benefit to craft meticulously with C. The feel of coding closer to the hardware is great.
C is closer to the hardware in the sense that with any given C code, I can pretty much predict what the generated assembly will look like. You cannot say the same for, say, Python.
Not really... Not only the generated assembly code heavily depends on the compiler (LLVM, gcc, Intel, MSVC...) and the optimization settings, you cannot be completely sure what the machine code generated by the assembler even looks like these days.
> The ideal language would be one I can memorize, and then never have to look things up.
If the language is so small I never have to look up anything when using it, it's also so small I'm going to have to spend ages re-re-re-implementing basic functionality that I get for free from a higher level language.
I had never considered a "data-centric" approach to be anti OOP but from the article:
> I am not an OOP convert. I've spent most of my professional life working with classes and objects, but the more time I spend, the less I understand why you'd want to combine code and data so rigidly. I want to handle data as data and write the code that best fits a particular situation.
I've come around to the idea much more that data should be the privileged structure and code should be considered transformations of the data. In some sense, code should be light and interchangeable and let the data is the first class citizen.
Are OOP and "data-centric" approaches at odds with each other? Does OOP only have validity when the data is relatively "small" and can be abstracted away easily?
> Are OOP and "data-centric" approaches at odds with each other?
Yes, because OOP Objects are data plus code (methods). If you don't have methods, but just pass data around, you don't have OOP. If you do have methods, you meet the "expression problem".
If you have static types and well typed code you get pure data at runtime, and if you include unions and product or sum types at runtime, you end up with algebraic datatypes, and if you have RTTI and polymorphism and inheritance then you get classical OOP.
If you have just data living in memory at runtime, and just transformations of the data (functions) sitting in your code, then you get FP.
I think this discussion is incomplete without a mention to scripting. (Writing the core engine in a low-level language like C but implementing some or all game logic in a higher-level garbage-collected language such as Lua or Python)
This line is pretty funny: "C++ is still the most common language for writing games, and not without reason. I still do almost all of my contract work in it. I dislike it intensely." hahaha poor guy
He dismisses C# without even knowing what options he has. The latest stuff Unity has been doing is heavily biased towards data driven design where almost everything is a struct. There are very few classes to be found that use any kind of polymorphism, and it is actively discouraged.
You could easily treat Unity as a cross platform rendering engine and asset loading system, and then build your own engine on top of it. I see very little reason to drop down to C or C++ at this point, especially if your goal is to ship your game to as many people as possible.
As a Unity dev this is really generous to Unity. After all it's extremely complex in a way the author would hate.
That said, I'd love to see a pure C# engine on .NET core 3 that has access to Span<T>. C# is set up as a great high level language with low level features as long as you're willing to do the work you would have done in C or C++.
Any game engine evolves towards serious complexity as time goes on. The question is, do you want to waste time building systems that a million man hours have gone into to make them resilient across a thousand different permutations of hardware? Or do you want to ship your game?
I've built a non-trivial engine active from around 2010 to 2015 that targeted all iOS and Android devices. Our test fleet of devices was enormous because we'd run into silly issues that were always device specific. My favorite, one particular phone with one particular driver version for the GPU did not correctly implement the ES 2.0 standard for constant defined variables. It caused our games to crash, and because of the scale of mobile this affected hundreds of thousands of users.
At this point you could not pay me to build an engine, because for anything that isn't a toy you end up having to solve these problems yourself.
Treating Unity as a rendering engine and asset loading system is not immediately "easy", but it's a very far cry from "hard". Dismissing it or other options simply because of unfamiliarity or a shallow distaste for OOP is ignorant at best and actively misleading otherwise.
Blackbird Interactive did that for Homeworld Deserts of Kharak so they could have a deterministic simulation for multiplayer RTS and used Unity for the rendering.
I've been surprised and delighted by C recently myself; Being able to make a small change to a datatype, variable name, and so on, and then instantly seeing an IDE highlight errors relating to it is amazing for productivity. The language is incredibly simple. I mostly use structures to define my own 'types' and design functions to work on pointers to those types. That way you can control whether you want your data to live on the stack or heap and its fairly robust. I've rarely if ever had to use function pointers or macros, but they're there if you need more advanced features. I find just being able to manipulate memory and buffers as raw bytes with no bullshit wrappers helps you so much with actual programming (TM.)
As far as data types go, I haven't used much more than linked lists and basic hash maps so far. For everything else I use fixed-sized arrays. It helps you precisely control how much memory the program is using without worrying that some higher-level magic garbage collector will overflow. Despite having that level of control, I haven't been frustrated by it yet. Yes, the standard library is very basic, but you can add the types you need very easily. There's some beautiful C code out there that will give you exactly what you want without having to include hundreds of obscure futures you'll never need. Overall, I've been very happy with it, and I look forwards to trying to run my code on all kinds of weird and wonderful platforms. I see huge competitive benefits for C code bases, to be honest.
They didn't dwell on why they chose C over C++, in much detail. In my view the conveniences and quality of life features provided by c++ makes it a better choice than C for large programs.
Also there are many kind of games that do not require a real time user interaction including many turn based games, card games, etc, so the type of game could be a factor in the selection of the tool. Some time s I guess game design can incorporate pauses and delays that might be required for technical reasons.
One of my dream side-projects is to create something inspired by Ken Thompson's C (the plan9 compiler), with some ideas borrowed for Go, where this "almost C" language could be written in a valid C just like you can do it with C++.
int DoThis(Child* this, int input) {}
Child* c ...
c->DoThis(10)
(Someway to define public/private in structs and methods, like Go do with convention of Lowercase/Uppercase components/function names)
and Finally
(Someway to define a interface and use vtables when needed just like Go´s Interface but in a more C compatible way like.. )
struct MyInterface {
int (*Read)(int a, int b);
int (*Write)(int a, int b);
}
or
struct MyInterface {
int Read(int);
int Write(char);
}
struct X {}
int Read(X* this, int n) {}
int Write(X* this, char c) {}
X x = X{};
x.Read(10);
x.Write('b');
MyInterface* c = &x;
(...)
(templates) - C++ way is fine
Thats it. With all this you would have "a better C", with compability with C codebase and still a language much simpler than C++, yet powerful.
Can anyone explain me why this is on the front page? I mean it's basically someone bashing languages they've not worked with, not understand or in the case of C++ simply don't like. As in, he doesn't bring anything new or insightful to the table, just "this is my personal opinion". Which btw is a fine opinion, as far as personal ones go, not one that I share, but it's a perfect example of "great let's agree to disagree".
Which IMHO basically amounts to trolling for HN comments if you post it here. Now people are just going to argue how wrong he is, against and for their favourite languages and nobody will learn anything new or useful.
Is this guy well-known or something? Did he make any good games in C?
Maybe I'm missing the point of this article (someone please explain), but it looks like it's worth as a post is only as a discussion starter and an inflammatory one at that. If there was any time a reason to flag a discussion thread for being useless, it's this one I think.
We see articles not so different from this concept all the time on HN. Recently it's about ORM vs SQL.
The core interfaces are really not terrible at all. They do have danger zones, but their complexities are relatively low. As the author of this article suggests, it's within the realm of something we can remember. That's a BIG deal.
I haven't done C in a very long time. But with C, my recollection is that the biggest issue is to make sure you keep track of your memory allocation and releasing. Sure this is significant, but it's a direct concern. And if you abstract by one level your management of memory, it's a lot easier (especially if you include some testing).
I think it's important to periodically evaluate if our efforts to simplify a situation have actually alleviated or simplified real issues that were hurting us.
There's one important question left out: what kind of games does the author make? I can see how 'vanilla' C can be more than sufficient for basic 2D games, but as soon as you grow in complexity it can quickly become an insurmountable task to grok.
It's not 3D that's harder to grok, it's more that the nature of 3D games are often bigger with more environmental triggers and events that can quickly become very tightly integrated and difficult to make abstract compared to, say, C++ or other languages.
Yup. Quake 3 is basic. I remeber that the whole game had a "measly" 150.000 lines of code or so. It doesn't do a ton of things modern games do. The list of things expected from modern games these days in comparion is far too long to list here. These things have become elaborate world simulators. And all of these features add up.
The Unreal Engine is around 4 million lines without dependencies (e.g. PhysX, a proper audio engine, etc). You could try to do all that in C (good luck finding a good physics library with a plain C interface, btw.). But you need to have proper software design throughout the project and you will likely end up replicating some kind of inheritance or polymorphism scheme somewhere.
The Unreal Engine is less than a modern game. The actual game is obviously missing. Also, you need engine specific editing tools to make a 3d game that looks more advanced than minecraft. It is a mistake to exclude them just because the don't have to be shipped with the final product.
And Quake 3 was made by a team of roughly 20 people. Id Software was quite small until they started to work on Rage.
Quake's code is very tightly coupled to the actual game logic and difficult to pull apart and reuse for another kind of game. A lot of this can and often is more abstract in newer games. That said, quake is very well written and a good example for when C can shine.
Nobody? I write my games in C as well :P The only other reasonable choice would be Rust, but never got past "have to write bindings to my framework" stage with it so far.
Pretty much the only things I miss in C for gamedev are sensible string manipulation and lambdas/closures. Otherwise it works really well - I don't think I would be able to make my games so multiplatform (at the moment GNU/Linux, macOS, Windows, Android, Emscripten, Librem 5, Nokia N900, Pocket CHIP, Raspberry Pi, Steam Link, Nintendo Switch w/ devkitPro; more to come...) so easily with any other language.
What C only compiler were you thinking of? Most of the ones I can think of are toy compilers. If you're making a game, you're going to be using clang/MSVC/gcc.
For catching bugs early, at least as important as the compiler are
* Enabling additional compiler warnings and -Werror
* Checking for memory leaks with valgrind
* checking for Undefined Behavior with UBSan
* Using mix of static code analysis tools such as Coverity, PVS-Studio, and the Clang Static Analyzer
Assuming OP is correct, I learned something new today. I knew running g++ on .c would produce different results based on differences in the language like sizeof('x') and differences in extern. A quick google didn't turn up any differences between warnings between g++ and gcc. I'll have to look into this.
The point is to eschew the use of C features not supported by C++ in order to catch bugs. Just read up on the incompatibilities deliberately introduced by the C++ committee and you'll likely agree that these are good things. Implicit conversions to void*? No thank you. Tentative definitions? I don't need that so give me an error.
Compilation speed for C++ is a function of the complexity of the code being compiled (particularly templates), otherwise a C++ compiler will typically compile C code (if it is compilable) at the same speed as a C compiler.
This is a surprisingly eloquent essay on the state of modern programming languages (and obviously why the author things that sticking with C is a better choice... and I'm fairly persuaded.)
I'd like to voice my disagreement with the author about C having the shortest compile times. From my own experience and reports all over the Web, it seems that Go handily beats C at compilation speed.
This is mostly down to a lot of design decisions in Go made with the goal of speeding up compilation. Elimination of header files, pre-compilation of Go modules, efficiently parseable syntax and more.
From what I've seen, first-time compilation of Go programs competes well with C, while incremental compiles are practically instantaneous.
The author despises OOP, and I agree with him there. I would add that OOP can be particularly bad for games, because often times the OOP vocabulary clashes with the game's own vocabulary: the game itself has objects (as in, things the player can pick up and put in their inventory), the game itself has classes (as in RPG classes). It might even have factories, depending on the game.
All support for OOP would vanish overnight if people realized you can do all the important OOP stuff using C structs.
> the game itself has objects (as in, things the player can pick up and put in their inventory), the game itself has classes (as in RPG classes). It might even have factories, depending on the game.
This makes no sense to me.
By that logic, it would be difficult for me to create a scheduling system for classes being taught in factories with OOP, which it's not.
For a program about classroom-scheduling, it would be nice if you could use "class" as a structure name. Then you could declare a function as taking a "class" as an argument. But if "class" is a reserved keyword in the language, you have to come up with some other name like "_class" or "ClassStruct".
I didn't mean to say it's an earth-shattering obstacle. Just one minor additional nitpick to add to the gigantic pile of problems caused by OOP.
> the game itself has objects (as in, things the player can pick up and put in their inventory), the game itself has classes (as in RPG classes). It might even have factories, depending on the game.
Am I an idiot for not knowing for sure this is a joke, or is the poster an idiot for not intending a joke?
It's not intended as a joke, and please have patience with me if I'm an idiot.
Imagine you had a program involving, say, chairs. It has things called chairs, which you pass around to functions which take chair arguments. Suddenly, the next version of the language comes out and now "chair" is a reserved keyword, and all your code is broken. To fix it, you have to go through your whole codebase and rename all your "chair"s to "ChairStruct" or "_chair" or something.
I didn't say it was a major world-stopping difficulty. It's just one additional nitpick against OOP for games (added to an already gigantic stack of much bigger problems caused by OOP).
I don't do much in C anymore and definitely never used a library that had a typedef'd pointer type. If I had to guess I'd imagine it's something like: variables that are pointers are syntactically different. Unless they're used "opaquly" I guess.
If I see a declaration like "Foobar baz;" I don't know which of "baz.boo", "baz->boo" or "baz[x]" are syntactically valid at a glance.
Dunno if that's a huge deal but it's all I came up with.
The thing is, even with the raw pointer the hard part is resolving ownership questions, not resolving the type. The tricky questions in C are: am I responsible for freeing it or is the library? If I send it into this function call am I transferring ownership? Type deffing doesn't make any of that easier or harder to figure out. Whether the thing is a handle or a pointer doesn't really complicate things, if that makes sense.
I get what you're saying, but I'm speculating what not type defing might help with. It's clearly advice that drifts around. Even if you think it doesn't help much it must address something for somebody, right?
Yeah. Like I remember the win 32 API (not saying it's a good API though) to typedef every struct and it's pointer. I'm not sure if the Linux API are the same though.
> Haxe feels much more promising than most alternatives. If I do web stuff again I'll be diving in here. There is some good library support. I am a little concerned by its relative youth, will it last?
Haxe has actually been around since 2005 or so. I'm really liking Haxe as a general-purpose compile-to-just-about-anything language.
> The strongest thing on my desired, but not required list is simplicity. I find looking up language features, and quirky 'clever' api's incredibly tiring.
Indeed. Clever apis are a disease endemic to the JavaScript community. expect(me).to.puke.when(i.see.shit.like.this());
> The ideal language would be one I can memorize, and then never have to look things up.
You can't memorize C. The idea that you can "hold C in your head" is a myth, and propaganda repeated by C pushers. In reality, the gotchas surrounding undefined behavior are so numerous and subtle that you're much better off using something else, even if it means accepting the complexity that is C++. (In reality, what you want is a language such as Rust that has well-defined behavior.)
> I need a platform that I am confident will be around for a while.
Don't get your hopes up. The closest we've had is POSIX, but POSIX is showing its age, and in the modern era churn is the rule, rather than the exception. More than a decade old? Rewrite it! New framework came out? Rewrite it! Developer fell in love with some flashy new technique? Rewrite it! Not enough nonbinary PoCs on the dev team? Rewrite it!
I don't think C is as bad as you're making it out to be. It's a small language. There are some quirks, but I don't think it's any worse than c++. You can even easily become a standard zealot (just look any c forum).
Rewriting software sucks. We usually don't like solving the same problem more than once.
I still believe that C++ is great because you can "use the good parts".
To me the STL containers are good enough. It's always a balance between how much time you save, and how much performance you lose. C++ is good enough with this. The only lacking thing would be an object pool.
The author writes
> but so simple it's not too hard to learn to use it carefully.
I would really love a replacement to C++, or an subset of C, that have the best of both languages. I can't use C because it becomes hairy very quickly, since you don't have things like STL containers.
I don't like Rust for the exact reason I quoted the author. The learning curve of Rust is too step, and its syntax if to distant from C. C is great because it's easy and fast, but it's also lacking many modern features that makes the programmer's life easier, without stomping down on performance or customization.
It's true that C++ is very complex, but I still believes it's very very usable, and the standard is improving a lot.
Yeah I agree that there's still room for a "better C". Just C but with a few modernizations would go a long way.
I think Rust has a different lane: Rust is for when safety is absolutely important. C is for when you want very little abstraction between you and the hardware, and you just want it to do what you tell it.
I get that this is not a popular opinion these days, but I like C. Quite a bit, actually. I can get stuff done reasonably quickly, don't have to futz around with multiple inheritance (C++) or where to call out to C anyway (python). There is so much more tooling behind C than Rust, or Go, or Zig (though I've been looking at Zig as a replacement). C isn't as bad as everyone makes it out to be, for personal stuff. I'd be a little more careful in group projects, but there's a reason it's still around. You do have to be careful and do a good design, but you should be doing a design anyway.
Some day, C will be deposed. Something better will show up and gain traction. On that day, I will happily switch to the new better thing; but for now, C meets my requirements pretty well.
I didn't even realize C was so unpopular till recently. I kinda figured industry wise, C++ was much more common, but there seems to be a lot of stuff still written in C (i.e. many widely used kernels). I can understand was people would be apprehensive to C, but C++ just seems extremely bloated and I just haven't been able to get into it.
I can see the value in C, especially for API wrappers and such, but it's entirely possible to just use a minimal set of C++ and avoid bloat. Simple templates, a vector math library with operator overloads, function overloads and std::vector alone are most of what you'll probably want to use from C++. Maybe std::unordered_map as long as you preallocate space for it. It doesn't have to be hairy.
It’s not unpopular to like C, and nobody ever said C is bad for personal stuff. Legitimate criticisms of a tool aren’t meant to be taken personally. I can say: “C happily lets you write off the end of arrays”, which is a valid concern, 100% true, and has no bearing on whether or not you should use the language.
I like C as well. I’ve been working it for a long-ass time, enough to be comfortable with it. Two immediate things I would love would be overloading (but maybe not) and something lighter compared to ICU4C when dealing with text processing. There are nice things like SDS, but not even close to what ICU does. It might not even be possible to do everything ICU does in less weight anyways.
I thought it was interesting that he highlighted Haxe as being a young language. I suppose it is compared to C, but I remember reading some tutorials about Haxe game development almost a decade and a half ago.
> Can D be used to make games? Yes. Has it been used in a major game release? It has now. Remedy Entertainment have successfully shipped the first AAA game to use D code. And it’s in a fairly critical subsystem too. This talk will cover the usage of D in Quantum Break, problems encountered and solved, and where we want to take our usage of D in the future.
>>I am not an OOP convert. I've spent most of my professional life working with classes and objects, but the more time I spend, the less I understand why you'd want to combine code and data so rigidly.
He doesn't want to combine code and data rigidly, so he uses a language without any concept of generics.
I think he doth protest too much, he can write code however he feels like, and Im not going to deny his personal opinions, but the justifications and reasons for those opinions are all just uniformly nonsense.
Makes some really great points, similar to why C++ is not favored for embedded. C++ compilers are just too unpredictable and when trying to meet code limits in 128K, 32K or even 2K of flash, C++ is a nightmare. All of the major embedded SDKs (STM, Microchip, SiLabs, TI, NXP, I could go on) are written in C... and C only. MBED is C++, but I've never encountered or heard of an OEM or integrator using MBED in a real product.
I use Keil, IAR, Renesas's compiler, XM8/16/32 from Microchip, and GCC.
Which do you use?
One of the main features of C++, its object-oriented stuff, is built on top of constructors and destructors -- aka dynamic memory allocation. This sort of thing is not desirable on deeply embedded systems due to resource constraints and the very real spectre of heap fragmentation -- embedded stuff tends to rely on static memory allocation done at compile time or system startup.
C++ isn't quite as portable as C -- Not just the compilers which never quite supported the same combinations of features -- even if you just stuck with the ISO-ratified features -- but the language runtime, standard [template] library, and headers are far larger with various implementations including plenty of mis-features and quirks. All of this meant that it was quite common for C++ code to not even compile with a different toolchain -- especially when templates were used -- and even if it did compile, it might not actually function correctly.
If you restricted yourself to avoid the problematic areas of C++ (eg not using templates or dynamic allocations) you'll end up with a language that is basically C. So why not just use C? :)
As you said, there are no vendor SDKs out there (that I know of, anyways) that are C++. But it's not terribly hard to integrate them in to a C++ codebase / roll your own abstractions because you're dissatisfied with the ones the vendor gives you (;
Different compilers supporting different subsets of the standard is definitely an issue, and it can be a problem if you're trying to use the same code with several different toolchains.
GCC in particular however has been really good about keeping up with newer versions of the C++ standard. I like and use many of the things on this list (excepting the standard library features, which of course you usually can't use off the shelf because dynamic memory allocation): https://github.com/AnthonyCalandra/modern-cpp-features.
In particular, features such as constexpr lambda, if constexpr, and type traits are nice ways to do some compile-time computation while being type safe - they're a lot more pleasant to work with than C preprocessor macros.
And of course, templates. They're a nice way to express certain concepts and reduce duplication without making any impact on your code size. But only judiciously, otherwise you will wreck your compile times (;
> Makes some really great points, similar to why C++ is not favored for embedded. C++ compilers are just too unpredictable and when trying to meet code limits in 128K, 32K or even 2K of flash, C++ is a nightmare. All of the major embedded SDKs (STM, Microchip, SiLabs, TI, NXP, I could go on) are written in C... and C only.
OTOH Arduino is C++ and works fine for millions of people, I've also dabbled a bit in various AVR, STM32 and ESP8266, all in C++.
Arduino is a sandbox for people with little experience in embedded programming to dip a toe. They aren't interested in writing optimized constrained code, which is what 99% of embedded programming for products is about. Two totally different spheres.
One thing with Rust is that it pretty much requires use of entity-component-system. It's the best practice for real-world games anyway, but people who write their first game are surprised they can't just "wing it" with some ad-hoc OOP.
C is one of my favorite languages but you know what be almost perfect for the author’s needs? OCaml! Almost as fast as C, no mandatory OOP, great performance, C FFI, nice FP, totally portable and native executables.
Unfortunately OCaml is single-threaded, has a stop the world GC (though if it’s good enough for Jane Street it’s probably good enough for you), and sometimes has unfortunate syntax (though ReasonML fixes a lot of this). Also OCaml doesn’t have the greatest type system in the world. No higher kinder types and instead we get the parameterized “functor” module system. Which honestly, kind of sucks. A lot of OCaml is an artifact from the past, if it could be remade today it would really be something great. Even so, OCaml is a great language and doesn’t suffer from a lot of the dogma that Haskell succumbs to.
I’ve always thought of and used OCaml as the functional programming equivalent of C. No unnecessary bullshit, just straight coding.
While OCaml boasts very low GC latency, I believe F# would be a better choice for writing games. It is an even more straight to the point OCaml; a Windows first class citizen; has a concurrent GC and I've heard C# is pretty popular for writing games due to Unity, so one might be able to leverage that (I don't know).
A non CLOS heavy SBCL codebase would also be excellent.
So I dug some more, and it turns out that while he does have something to say about Rust, I'm not sure I'm convinced by his conclusion about safety:
"Safety. Yes, Rust is more safe. I don’t really care. In light of all of these problems, I’ll take my segfaults and buffer overflows. I especially refuse to “rewrite it in Rust” - because no matter what, rewriting an entire program from scratch is always going to introduce more bugs than maintaining the C program ever would. I don’t care what language you rewrite it in."
(I think it's a bit mean to downvote me for not knowing that he addressed that point in a separate page of his. He didn't link to this Rust review at all!)
Holy... Believe nothing that guy says. I'll just wait until he releases his first game with network connection and instantly gets a buffer overflow and his game becomes an entry for worms into people's systems.
> and ideally I'd like to have the option of developing for consoles. So it's important that my programming language is portable, and that it has good portable library support.
Every console will have a C toolchain out of the box, we can't say the same for rust.
”Similarly I want to avoid tying myself to a particular OS, and ideally I'd like to have the option of developing for consoles. So it's important that my programming language is portable, and that it has good portable library support”
Availability of Rust across platforms is nowhere near C or C++. It may, and I hope it will, change in the future but right now it is not so.
It's quite curious that he didn't mention Rust as a possible alternative. Still fast compilation, still no forced OOP bloatware added, plus static memory management that prevents the issue with garbage collection that happens in Go.
Quote: "I want to produce less bugs, so I want strict typing, strong warning messages and static code analysis".
Yeah, at strict typing you lost me buddy. Let's just go with somebody else reply, as in you like C and that's why you do your hobbies in. Nothing wrong with that in the end.
Implying that typing does not prevent bugs is the strangest programming meme I’ve ever heard, and its proponents push it so hard that I wonder if there’s somewhere I can sign up to be paid for it. We rarely are able to directly compare the cost/benefit of strict typings vs loose typings, but with JS vs TS you get a pretty direct comparison, and it is absolutely unsurprising that TS is eating the JS world; it does prevent bugs, it does improve productivity. Tests do too when done correctly, but both together are better than either individually.
When you go further, you can see that the core innovation of Rust is basically adding ownership to the type system and therefore making typing even stronger. Strong typing is a win for less bugs. Full stop.
I too do not understand how anyone can complain about typing if they have used a statically-typed language for any real projects.
Your TypeScript versus JavaScript comparison is about as good as you can get. Many people I've met who think duck typing is the preferred way to go do so because they learned to program on JavaScript. That is more familiar to them, TypeScript is an extra burden to getting work done, and therefore is bad.
I am working on a large project that uses TS (Angular) on the front-end and I can't possibly imagine doing that in plain JavaScript. The lost hours to simple, avoidable bugs would be too great.
Strict typing afaik isn't really a formally defined, imo.
As you know C is statically typed and generally said to be "weakly typed" as well but you can make a strong argument that just about any language is "strong" or "weak" -ly typed based on a dozen or so characteristics. I think it makes more sense to treat it as a spectrum where some languages are "stronger" than others. Which is different than in the case of "static typing" which you either have or don't.
Oh come on, I was taught the difference between strong and weak typing with C (weak) and Pascal (strong) as the examples 30 years ago. You can cast a C entity to something else and NO ONE ELSE KNOWS.
While Pascal is indeed strong typing and C is weak typing, casting is allowed on both at compile time and introducing bugs this way is not going to be stopped by compiler. Here is what a compile will behave in C vs Pascal:
C code:
int i = 1;
char s = "1";
{
if (i == s) //<--- this will be allowed by compiler in C because C is weak typing, and will comeback later to bite you at runtime
{
//bla bla
}
}
Pascal code:
var
i : integer = 1;
s : string = '1';
begin
if i = s then begin //<--- this will not be allowed by compiler in Pascal because Pascal is strong typing
i think op meant that it’s strange to want strong typing and opt for C at the same time (because C is far less strongly typed than other more modern languages i suppose). Not that strong typing isn’t a good thing.
I implied that he chose C for strong typing, which is not. C is weak typing. You want strong typing, you go with Pascal instead for example, that's a strong typing language.
I'm sure typing prevents some bugs. But if I'm looking at the bugs we find in the JS projects I work on, only a very few would have been avoided with typing. The vast majority of bugs are about unsound business logic, wrongly understood requirements, etc.
That’s because the thousands of errors were manually fixed before committing.
I’ve programmed professionally in both Haskell (strongly statically typed) and Racket (dynamically typed). The number of bugs I encountered at runtime with Haskell was very close to 0 per 1000 lines of code. For Racket on the other hand, I would get maybe 5-15 per 1000 lines. Most of these were easy to catch and never made it into production, so it really only wasted my time instead of making the app unstable. But there would be a few very stupid bugs that would have been easily caught with static typing on the unhappy/rare path that wouldn’t be found until weeks or months later.
Nowadays, I try to avoid all jobs that use languages with dynamic typing on production software simply for the reason that they make me anxious and upset. Maybe everyone else is just so much better than me that they never produce typing relating bugs, but I kinda doubt it.
If you have a super duper type system you can use it to prevent lots of bugs in business logic, and you can use it to do a lot of work for you - as a solver. There are levels of value in typing - bug finding, but then also communicating what you are up to to other people using / reading the code (including you in < 6 weeks time!)
Definitely another reason to stick to C. In C you don't have to change to another language or another framework, or yet another design principle, or whatever hype that is being followed by a horde of idiots that think they're incredibly smart.
C is still C. I love that so much. No endless discussions about type safety. And yes, with C I can shoot myself in the foot, which is great because I like sharp tools that can cut. Tell me of a cook who prefers a blunt knife. But at least, I'm the one doing it in contrast to web development where you use hundreds of amateur libraries that kill you in a snap, without you ever finding out what actually happened.
You're going to get buried for the odd knife metaphor :/
Even if we ran with it, like, would a cook use a knife with a sharp handle because it's "more sharp"??
I get what you mean though: C is pretty liberating. It gets out of your way and let's you be pretty precise about what you want it to do. In return you have to... well, be precise. If you have a bug it feels like you just need to get better. If a language makes too many promises and fails you, it feels like it let you down.
But I feel that way about Java vs. Scala, so what do I know.
The problem with C isn’t you shooting yourself in the foot. The problem is that off-by-one errors and buffer overruns that are trivially easy to produce in C turn into security bugs that affect your users.
If you don’t have any users, fine, but no one writing a nontrivial program for use by other people should be doing it in straight C at this point if it’s at all possible to avoid.
> "C is dangerous, but it is reliable. A very sharp knife that can cut fingers as well as veg, but so simple it's not too hard to learn to use it carefully."
After I got past the horrible misspellings and atrocious grammar, I could appreciate what the author is saying. The author is privileged to be able to concentrate on and enjoy a particular language. That is great, for him.
Most of the rest of us have to learn and use a wide swath of languages, features, and technologies to stay a leg up in this world. We seldom have the choice to pick what we work on. Sure, we can change jobs, but inevitably it requires conforming to someone else’s opinion.
The author makes me want to go back and pick C up again. But like so many times in the past, I will probably find it needlessly painful and go right back to the more modern languages which already solved so much for me. I don’t mind standing on the shoulders of past C developers. I, for one, also do not mind learning the details of any particular modern language because, if anything, it makes me more marketable.
I love this opinion from games programmers because they never qualify it and talk about what their latency budgets are and what they do in lieu of a garbage collector. They just hand wave and say "GC can't work". The reality is you still have to free resources, so it's not like the garbage collector is doing work that doesn't need to be done. What latency budgets are you working with? How often do you do work to free resources? What are the latency requirements there? Even at 144 fps, that's 7ms per frame. If you have a garbage collector that runs in 200us, you could run a GC on every single frame and use less than 3% of your frame budget on the GC pause. I'm -not- suggesting that running a GC on every frame is a good idea or that it should be done, but what I find so deeply frustrating is that the argument that GC can't work in a game engine is never qualified.
edit: wow for once the replies are actually good, very pleased with this discussion.