4 ms·
Not sure how this works under the hood but if I made a game in .NET I'd basically have a ban on any per-frame allocations. That means you have to be quite non-i
by alkonaut 4y ago
Not sure how this works under the hood but if I made a game in .NET I'd basically have a ban on any per-frame allocations. That means you have to be quite non-idiomatic when it comes so C# within the game loop, e.g. no dynamic collections (resizing will allocate), no "foreach" risks allocating enumerators. There are very few "zerp cost abstractions" in C# so when you want zero cost, you must also avoid some abstractions. Basically you use a very C-looking subset of C# for the parts of the code that run in the game loop. Allocate everything up front, use pools and arenas, etc. etc.
It's easy to trip up but tooling is good so it's easy to notice when you accidentally introduced allocation. The engine isn't the problem, it's that when you expose C# as your scripting language then users might think they can write "normal" C# in their game scripts. Which you probably don't want to do.
- bob1029 4y ago> I'd basically have a ban on any per-frame allocations. I have a prototype where I intentionally invoke GC after every frame. My reasoning is that some amount of GC will eventually need to occur, so why not do it as frequently as possible and at the exact moment where we can compensate with other measures, such as the frame scheduler & time step abstractions. Even with heavy allocation (i.e. deserializing the scene graph from a wire protocol before every frame), this approach seems to keep GC pauses bounded to ~1-5 milliseconds in my implementations. If I did not explicitly call GC when I thought best, the runtime would decide to run a 100-1000 ms collection at its convenience and the entire experience would become worthless to human perception.
- kevingadd 4y agoThis is a great strategy as long as you can keep the pause times low enough - especially if you manage to hide the gc behind vertical sync. Getting the exact timing for that is a little tricky, but if your game is running fast enough you've probably got upwards of 5ms waiting to be used while your main thread blocks inside Present/SwapBuffers. (Because those functions are native OS APIs, there won't be managed code on the stack and the GC should be able to run.)
- starburst 4y agoThis is NOT a great strategy, although better than waiting for the 100-1000ms collection you're still at the mercy of the GC which can be very random and you will miss some VSync for sure doing that even if you're supposed to have some "room". Missing a VSync in mobile means that the refresh rate will switch to 30hz for a brief moment, there is no in-between (ie; 54 FPS or something like that) which is pretty noticeable. The only "great strategy" is to have zero alloc (or close) while gameplay is running and if you want to go the extra miles, disable GC completely and call it only in some appropriate moment (level loading, bringing up the menu, etc.).
- bob1029 4y ago> The only "great strategy" is to have zero alloc (or close) This is the path I am taking. Ultimately, I would try as hard as I can to not allocate, but having a good plan for dealing with some per-frame blips is going to be essential. It gives me a reasonable starting point to work with on the journey to zero alloc.
- pjc50 4y agoHmm. How feasible is it to write Roslyn analyzers that tell you whether a function is going to allocate?
- notanaverageman 4y agoThere is already an analyzer for it: https://github.com/microsoft/RoslynClrHeapAllocationAnalyzer https://github.com/microsoft/RoslynClrHeapAllocationAnalyzer
- deleted 4y ago[deleted]
- deleted 4y ago[deleted]
- olvy0 4y agoThat Roslyn analyzer linked by notanaverageman below is really cool, I didn't know about it. But I don't think that even that analyzer will be able to tell if you call a method which itself allocates memory. But it's not very complicated to write a simple IL analyzer, using Mono.Cecil to go over the raw IL, instead of using Roslyn. The idea is to look for the newobj or box IL opcodes, but also to recurse to called methods (when meeting the callX opcodes), by finding using Cecil where the call leads us, and loading that method. This could be used after compilation instead of during compilation, but it can be used without actually running the code, so it's a sort of static analysis. I threw together a simple demo, that has some bugs but is generic enough and should work. It's not very efficient though. See here : https://dotnetfiddle.net/EyuYSh https://dotnetfiddle.net/EyuYSh (It can also be run from there since dotnetfiddle supports nugets). And also in godbolt: https://godbolt.org/z/P3rK1voKP https://godbolt.org/z/P3rK1voKP (it doesn't run there since Godbolt apparently doesn't support nugets) It's self contained and will run as a console application by just copying and pasting the code into an IDE. You need to add Mono.Cecil as a nuget package though.