Why I Write Games in C (yes, C)
jonathanwhiting.com
jonathanwhiting.com
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.
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.
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?
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.
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.
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.
Most times it may run in 200 micro-seconds, but the one time it takes 20ms the user suffers from unacceptable stutter.
I can see how people would want to bypass the problem entirely by being a bit more careful up front.
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).
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.
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.
Disable the garbage collector:
debug.SetGCPercent(-1)
Trigger garbage collection: runtime.GC()
It is also possible to allocate a large block of memory and then manage it yourself.At which point you're mostly just writing C in Go.
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.
There may be more "C programmers" by number but a Golang codebase is going to be more accessible to a wider pool of applicants.
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.
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.
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 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).
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…
GCC also supports Go (gccgo) and can call native libraries just like from C.
I'm not saying there are no drawbacks in Go, just that I can't think of any advantages of C over Go.
Go is a good language for web backend and other network services, but it's not a C replacement.
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.
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.
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.
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?
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.
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.
(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.
Many C/C++ allocators don't release to the OS often or ever.
If the program runs for any length of time, it will probably need the same memory again, so freeing it is a pessimization.
Standard C library free() implementations very, very rarely free memory back to the OS.
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.
All of these ideas require more care when using the library though.
https://github.com/mrdoob/three.js/blob/master/src/math/Vect...
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 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.
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.
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 think they posted the guidelines and it was a wonderful read about how they developed real time systems.
Never experienced the malloc() thing but do throw exceptions and fail fast under conditions like these so they're caught in testing.
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.
> a system where malloc() was forbidden. In fact it always returned null.
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.
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.
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.
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 :-)]
Hah. Who sez that audio and video products have 'soft' real time? Go on now.
I assume some dedicated devices are more or less hard real time, due to running way simpler software stacks on dedicated hardware.
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.
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.
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).
In terms of customers and how much they care, the North American market seems to care less than Europe.
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.
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?
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.)
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
It's successful in spite of that, but that doesn't make it any better.
You might spend more time fighting the GC than benefitting from it. And that seems to be the experience for large games - simpler ones might not care.
Unity offers a lot more than just a language, and developers have to choose, are they willing to put up with GC to get the rest of what Unity offers.
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).
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.
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)
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.
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)
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.
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?
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".
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.
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.
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).
At the expense of a lot of work from the GC. In production we had 30% of the usage coming from the GC alone…
But for game development the problem isn't going to be the GC anyway: cgo is just not adapted to this kind of tasks (and you cannot avoid cgo here).
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.
The trick here is to double the heap size, which would be completely unacceptable for a game that's making use of what the hardware offers.
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.
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`.
If you need to store pointers and want to conserve a bit of memory, perhaps my compact_arena crate can help you.
Is there a particular codebase you are thinking of here?
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.
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.
https://blogs.unity3d.com/2018/11/26/feature-preview-increme...
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 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.
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.
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.
Test was on an i9 7900X.
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.
https://www.google.com/search?q=memory+management+site%3Agdc...
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.
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...
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.
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.
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.
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.
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 :)
do you program a lot with your feelings ?
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.
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...
Somewhat related: I recently wrote about garbage collectors and latency in network drivers in high-level languages: https://github.com/ixy-languages/ixy-languages Discussed here: https://news.ycombinator.com/item?id=20945819
That's a scenario were short GC pauses matter even more as the hardware usually only buffers a few milliseconds worth of data at high speeds.
Memory management is ultimately far simpler and easier in C++ than in C#.
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.
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.
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.
People went to extreme measures to avoid allocating memory in their games: manually pooling every in-game object & particle, not using string comparisons in C#, etc https://danielilett.com/2019-08-05-unity-tips-1-garbage-coll...
Unity itself finally has a new system they're previewing to average out the GC spikes over time, so a game, say, never drops below 60fps: https://blogs.unity3d.com/2018/11/26/feature-preview-increme...
As well, there is a new way of writing C# code for Unity called ECS that will avoid producing GC sweeps https://docs.unity3d.com/Packages/com.unity.entities@0.1/man...
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.
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
https://docs.unity3d.com/ScriptReference/Scripting.GarbageCo...
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.
> 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.
https://www.eurogamer.net/articles/digitalfoundry-2016-firew...
https://www.eurogamer.net/articles/2016-02-10-firewatch-dev-...
Hopefully as the engine continues to improve dramatically, this kind of thing will be left in the past
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.
> 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.
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.
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.
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.
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.
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).
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.
In other words, the problem is GC, full stop.
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.
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.
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).
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.
Just have to say, loved that game to bits.
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.
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.
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.
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.
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.
It's common to use stack and pool memory allocators, where this becomes no longer expensive work.
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.
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.
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 program having a say in how the GC behaves would be one requirement IMO.
> you could run a GC on every single frame and use less than 3% of your frame budget on the GC pause
Could I? In what languages?
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.
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.
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.
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.
A few examples: https://handmadehero.org/ https://ourmachinery.com/post/physical-design/
Simple libs widely used in game dev circles: https://github.com/nothings/stb
This one is a full game engine with tools made for educational purpose: https://www.raylib.com/
I do write games and game engine code and tools in C++ without using any of the OOP features.
I know quite a few of other professional, experienced and talented developers doing the same, even full AA studios with up to 50 employees.
Some of them are going back to procedural C after a few years of disappointment with OOP madness.
I'll publish articles about this later, but for now, all I have to say is that it just works.
Newbies are often afraid of manual memory management and pointers, but there are simple ways to avoid shooting yourself in the feet with those.
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.
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.
D3D11_MAPPED_SUBRESOURCE mapped;
context->Map( buffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped );
// Write memory address pointed by mapped.pData
context->Unmap( buffer, 0 );
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.C++ with structs and 99% procedural code is actually very close to vanilla C.
Let's not be pedantic here.
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.
You understand that he is an extremely successful programmer in his field and has the respect of thousands of programmers world wide?
Maybe you can share some of your accomplishments with us.
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.
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.
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.
Defense in depth. Write good, sane, secure, fail-closed code when you want to ship that code to other people--or don't write code.
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.
I am far from an expert in security but I've done a bit of reversing/hacking when I was 16 years old (DOS era)
I think this is hard to check for everything, but in my opinion, good security can be achieved with extreme simplicity.
Packets are fixed-size, every fields are manually bound checked and the packet is rejected if anything looks wrong.
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.
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.
Many consoles have been "jailbroken" by making buggy games load specially crafted savegames that ended up in code execution.
That also extends to players downloading innocent-looking maps, savegames, models. etc that exploit bugs and end up in code execution too.
Also if you have an online game then you can have RCE bugs, especially if players are expected to be able to connect to 3rd party servers.
(Not saying I necessarily disagree with you, just a counterpoint)
That's an argument for using C in gamedev, isn't it? ;)
How can I see that list? I'm interested in comparing languages this way.
Excuse my C/C++ ignorance, but why not simply use C?
So by definition require OOP?
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
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.
https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attribute...
To avoid cache misses you can't use OOP, that's probably where this wave of going back to C is coming from:
For example here's how someone that knows how computers work writes Java games: http://www.javaonthebrain.com/java/warp/warp.java
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.
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++...)
Which is why it has been done for my 2D Second Life clone?
Some of us can very easily implement C++ stuff in native C.
Pointers which can't be NULL (aka. references)
Exceptions which also clean up intermediate frames (the exception itself can be done with longjmp, but no destructors which could be run)
A programmer who makes full use of destructors is simply a better programmer than one who doesn't.
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.
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 :)
O RLY ? https://gcc.godbolt.org/z/QhbUjI
I expect there is a way, just I don't know it.
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.
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.
For even more examples of this have a look at this talk when it comes out: https://cppcon2019.sched.com/event/Sfq4/there-are-no-zero-co...
(for example: https://gcc.godbolt.org/z/mU7hub)
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.
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."
I want to like rust but everytime I dive in it is getting more and more complicated and verbose.
Rust also becomes a lot less verbose when you get better at it. The ? operator is especially useful.
That's not even true for languages with dependent types, which Rust lacks.
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.
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.
I prefer to read and write simple C like syntax. I would love it if I can get Rust concept of borrowing in C, or even simple operator overloading..
macro brw($expr:expr) { &$expr }
And use it like 'brw!(x)' instead of '&x'.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.
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.
var x:i32 = 5; ugh, it makes no sense.
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.
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.
I would reframe this as the design of C++ makes it hard to write code that compiles fast.
> 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++?
You may find that not all C++ features are supported for the least popular processors or for old ones.
Standard C++ threads, for example, were not supported by GCC for Windows for a long time. I do not know what is the current status, thou.
> 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 one hand, it isn't C++, and it has real type safety, and it interfaces with existing C libraries very easily.
On the other hand, it is rapidly getting very complicated, and seems almost eager to become another C++ in terms of sheer volume of language features.
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 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.
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.)
Clang implements it, last I checked, meaning that it should be (at least in theory) portable to every platform Clang supports, no?
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)
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 goal (at least mine and of some authors following this minority trend) is to write and read simpler, more straightforward code.
Take a look at the code in the examples of the following url to see what I mean.
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 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?
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.
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.
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++.
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.
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.
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.
things like:
(Composing structs)
struct Parent {int number}
struct Child {Parent; int another_number} // anonymous struct convention
Child c;
c.number = 10;
c.another_number = 11;
(Object notation, namespacing) 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 fineThats 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.
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.
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.
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 far more than a modern game. You wouldn't call Clang and editor just because Vi was compiled with it.
And Quake 3 was made by a team of roughly 20 people. Id Software was quite small until they started to work on Rage.
And there's still plenty of life left in 2D anyway. Not everyone wants to crank out the next Fortnite or whatever the kids are into these days.
Civilization.
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 AnalyzerThis 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.
All support for OOP would vanish overnight if people realized you can do all the important OOP stuff using C structs.
Such as having a safe, leak-proof string type? I think not.
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.
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.
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.
Am I an idiot for not knowing for sure this is a joke, or is the poster an idiot for not intending a joke?
I'm so confused...
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).
Haxe has actually been around since 2005 or so. I'm really liking Haxe as a general-purpose compile-to-just-about-anything language.
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!
Rewriting software sucks. We usually don't like solving the same problem more than once.
Previous discussion: https://news.ycombinator.com/item?id=10870488
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.
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.
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.
FreePascal! :-D
> 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.
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.
Certain things are very nice about C++ as compared to C, especially C++11 on:
* type safety and expressiveness (aided by judicious use of templates)
* The constexpr keyword for real compile-time constants (again, type safety)
* RAII (often aided by statically allocated pools of memory from which one can then allocate objects from at runtime)
What kind of issues with code size have you had (and using which compiler)?
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 (;
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++.
I used to write games in plain C, and nowadays I'd definitely use Rust for them.
There is a small Rust games community and a couple of engines/frameworks: https://lib.rs/game-engines
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.
Writing C code is also writing C++ code, but badly.
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.
A non CLOS heavy SBCL codebase would also be excellent.
"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."
From https://drewdevault.com/2019/03/25/Rust-is-not-a-good-C-repl...
(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!)
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.
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.
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.
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.
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.
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.
Then THINGS happen.
BAD things.
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
//bla bla
end;
end.
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.
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.
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.
Cliche analogy.
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.