How “deleting multiplayer from the engine” can save memory

Although the title of this article might suggest otherwise, I don’t dislike Unreal’s multiplayer features. As someone who has worked on two different multiplayer games in Unreal, I enjoy Unreal’s seamless multiplayer integration. It’s built deep into the engine instead of being moved to an optional plugin like so many other features. You can test any project in multiplayer mode without writing a single line of code. Of course, it will not work properly without many corresponding changes to the structure of your game, but still: Multiplayer is in the DNA of the engine.

This has many advantages and makes multiplayer development much easier compared to Unity where you have to install and activate additional plugins and libraries before you can start working on multiplayer features.

But there must be some drawbacks to this approach, right? What if you were working on a single-player game? Is there a disadvantage from having all these features?

For the most part, no. Unused code/features don’t affect the performance of your game (at least not in a measurable way). But there are some small exceptions. One of them is the required data that needs to be stored in Unreal’s base classes for multiplayer to work. Mostly simple variables like “bReplicates” in the AActor class, which defines if an actor should be replicated over the network or not.

Even when developing a singleplayer game, this data still needs to be stored in every instance of the class, it remains unused. But so far, this mostly seems a theoretical optimization, not very worthwhile to do in a real project. This variable is even implemented as part of a bitmask, so we wouldn’t even save a single byte, it’s just a single bit of unused data. Doing any change to a working codebase to save a few bytes in memory? No thanks.

At least that was my opinion until I happened to show some students Visual Studio’s Memory Layout Visualizer, using the AActor class as an example. This tool displays the memory layout of any class, like this:

And only now did I stumble over the actual size of some multiplayer-only data:

Yes, to replicate the component attachments and the actor’s movement over the network, Unreal uses some custom structs that are 120 and 216 bytes respectively. Suddenly the idea of removing some multiplayer features from the engine to save memory didn’t seem bad anymore. If I could find some more data like this, it might actually make a measurable difference.

Going through all the data in the class, I found more and more variables that would be completely unused in singleplayer games. Things like the NetUpdateFrequency and MinNetUpdateFrequency (4 bytes each), the PhysicsReplicationMode (1 byte) or the list of “ReplicatedComponents” (16 bytes). After going through the whole list and doing the math, I got a potential memory reduction of 328 bytes. This would reduce the size of an actor from 1088 to 760 bytes (editor build)/from 840 to 528 bytes (standalone build). And yes, the difference is not exactly the same between the two build configurations because of alignment shifts, but I didn’t want to start moving things around on top of all the changes.

And now we are getting to the downside of all those memory savings: It’s not easy to do it in a clean way. My first idea was to exclude all these members via a preprocessor macro, so that I could switch back and forth between a multiplayer and singleplayer build configuration:

I really liked this approach when I tried it, especially since it was consistent to similar Macros like UE_SERVER that Unreal already uses to compile UI stuff out of dedicated server builds.

The problem: The Unreal Header Tool (UHT), that parses the code to handle all the reflection for UPROPERTYs doesn’t like it if you exclude or include UPROPERTYs based on some preprocessor macros and will abort it’s work when you try to do so. It’s definitely not impossible, for example the WITH_EDITORONLY_DATA macro does exactly that, but I would probably need to modify the UHT for it – something that I felt was out of scope for my experiment.

(If you by any chance are more familiar with the UHT and know how to handle such preprocessor macros, feel free to message me or leave me a comment. I would love to do a follow-up on this idea.)

So I resorted to commenting out the variables in question and all the code that used them. A mind-numbing but simple task. If you plan to try this optimization I would recommend to get a BIG coffee before starting this phase, it will take a while. Double that, if you do it in an actual production environment and have to document all your engine changes.

After modifying 58 engine files, my editor finally booted again and to my great relieve everything worked as intended.

How much did it save?

I’ve already told you that this method saves 328 bytes per actor, which is not too much at first glance. You can apply the same trick for SceneComponents and save another 32 bytes per SceneComponent. Assuming an average of two SceneComponents per actor, you get up to 392 bytes per actor. Still not an impressive number unless you deal with a lot of actors. A hypothetical example level with 25 000 actors (which is a lot, but not unreasonable) will save about 10 MB.

Is it worth it?

For most projects, I would say the answer is no. There are many, many other ways to save more significant amounts of memory that you should pursue first. But like with many other possible optimizations it’s better to have this option available to you than not.
If your project for some reason needs a high number of actors (> 100 000) and you can’t reduce this number with other methods like actor merging or Instanced Actors, this optimization might be more interesting to you than others. I have worked on at least one real project where this optimization would have saved 100 MB.

If this experiment was interesting to you, consider following me on other platforms like MastodonBluesky or LinkedIn for more articles about Unreal, game optimization or other gamedev topics 🙂 You can also subscribe to this blog directly below this article to stay informed about future posts.

Responses

    1. Shannen Avatar

      It’s not just the RAM usage it’s the CPU cache usage / pollution that their Actor / OOP implementation causes and is why MASS Entity is the future, atleast for core systems that require peak performance.

      That said while on one hand I loathe the idea, I do think there is a case to put forward to move a portion of the code out of certain Classes like Actor / SkeletalMesh etc into more lean parent Class(s) that serve only the “really core requirements” without breaking existing projects.

      Like

Leave a comment