Deep Dive: Memory Management in Blueprints
I wanted to start deep diving into how Blueprints runs memory and what optimizations I could make, so let's talk about that.
From what I've read, casting in blueprints does a number on memory. Apparently, casting to a blueprint forces that blueprint to load, whether we're using it or not. Let's use a personal project for this: Escape Fracture.
I mostly use interfaces for this one simply because I want plenty of interactability. Let's get our in-game physical memory usage in the ActionBlockTesting level to start out with.
So as we stand, we hover between 785 and 775 MB of physical ram usage. At this point, I feel like that 'peak' is at start-up, but we never get close to that number moving forward. I'm wondering if that's garbage collection doing its magic, but we still shouldn't be loading 100 MB we're not using, even if we release it later.
Let's do an experiment. I'm going to branch with a true condition, and execute a cast on true. I'm going to get into our FirstPersonCharacter and cast to a BP that we don't use in this level, if I can find one.
So we're just getting an empty reference and trying to cast it. I did see that before I moved in-game, the memory use was just under 1GB. Otherwise, nothing looks too different. Maybe this was garbage collection? Let's execute that cast on false instead and see what happens.
Nothing strange here, either. Maybe it's because we're using an empty variable? Let's try just passing in that player reference that we know is valid.
Nothing here either, except the physical used went lower then it ever has. We're even using less virtual memory than we were before. The peak is also down.
I decided to output a memory report to see what was going on under the hood. Now I'll admit, most of this is Spanish to me, but this caught my eye:
We're not using vehicles or weapons, so I'm not sure why these are being referenced.
I dove into the reference viewer for the first time to see if maybe we were still pointing to something here? I saw how many blueprints referenced our player, but that also makes me wonder...does upstream referencing also impact memory? If you are referenced by something, does it load that thing in no matter what?
I did note that we have a reference to the logic core here, though. That didn't go away when I removed the cast; I had to remove the comparison as well. Since I've been lead to believe that you do load anything you reference, that means we're loading in the logic core. So it loads into memory if we even need to compare it. I decided to try auditing the asset to see what size it is and realized something pretty obvious.
Our actor isn't big enough to have a consistent, noticeable difference in memory. That number seemed low to me, so I did a size map.
That makes much more sense. If I'm going to test this properly, I need a big, fat actor that's maybe up near a hundred megs. Something that'll really make the numbers shift. For the sake of good testing, let's get one more shot of the memory so we can solidify our baseline.
Okay. Now we're gonna make a big one. I want to try to get it to crack 100 MB. We don't have to load it in yet. I just want to reference it. It has to reference something no other actor is referencing.
I created an empty actor and added a skeletal mesh. That gave us 35 MB but that's not quite as much as I'd like. Nevertheless, if casts / references are loaded, we should see us hovering just over 800 MB. This time, I'm going to cast on overlap and delete those keys, which have overlap volumes. I'm suspecting that garbage collection is removing a cast that it knows will always fail.
Despite that, still no results. The highest it ever got was near 800, but it was inconsistent and it always dropped out.
So I went back to my original memory report and decided to do some digging. I realized that the pyramid that makes up our LogicCore (as well as a couple other blueprints) were all in the report the other components that make up the blueprint were also being loaded into memory. So. Let's try this a different way. Instead of relying on the numbers on-screen, let's use the memory report. I'm going to remove intentionally errant references and run a report. After that, I'm going to insert a bunch of actor references that I don't need (a bunch of casts will do the trick for this) and run a report again and use a textdiff tool to see what, if anything, is new. I'll undo those changes and run another report to compare against the first, just as a control. After that, I'll use my newfound knowledge to clean up as many references as I can, run a fourth report, and textdiff against the first. Here we go. So first let's look at the first and third tests for our control.
Not too many differences. I don't fully understand why we used so much more memory on the one to the right (our third time around), so I'm doing it a fourth time to see if it had to do with how much data was in the previous standalone. We'll compare our fourth with our first.
Still some big differences there, but Physical Memory looks a bit better. but you can see by 'removals / additions' that it's not much. The only other differences are down in the "time unseen / time alive" section.
It's just hit me that these reported numbers for physical memory and virtual memory are about my entire system (which I checked by booting up and checking resource use), so I need to be looking at 'process'. Sounds silly, but now I know.
Now let's look at the diffs on our first and second steps. The second step has the casts. That's the only difference between it and the others.
I could see there were huge differences. The process used an extra 20-40MB, which isn't huge, especially when you look at how our One and Three / Four processes compared in memory. We're also not looking at massive actors here, so the actual memory impact may be difficult to perceive; the diffs, however, speak for themselves.
I went and poured through them just to be sure. Since I casted to actors with skeletal meshes and animations, the additions in the second file consisted of anim blueprints, which made sense. Wherever there were differences between two existing pieces of data, the second file always had higher count and numKB / maxKB, confirming my suspicions.
It does appear to be that referencing an actor in any way, casting or otherwise, does put that actor into memory. So why does this matter and how do I solve this? Well, I'm going to take a look at my reference viewer and point out something I find troubling.
This power module is reference by a map and references a PowerSwitch. But we don't have a power switch in that map. That's a waste of time and resources. Is it a huge thing to account for? No. Is it bad practice? Absolutely. This has been absolutely eye-opening. Within about two hours, I now have a much greater understanding of how memory works in UE4 and how I might be able to make it more efficient.