In the documentation (Functions | Unreal Engine Documentation) it states the following: “This means that a Pure Function will be called one time for each node it is connected to”. However, according to my tests, it’s not how it works. Given the following example:
The long running joke with Pure nodes, is that they’re not very PURE at all.
In that they’ll happily reevaluate once again for every connected wire… But…
Are you saying GetExpensiveValue is now evaluating consistently in 4.22?
Same as if it was just called once or same as if it was assigned to a variable…
If so, maybe Epic reworked this after feedback. As it was filled with gotchas!
Whereas, you want a different unique value passed to Multiply / Addition etc?
(Either way, don’t trust the unloved docs - opt for empirical testing or nothing)…
That’s what I thought but I think both types are called “nodes”. Maybe it says it correctly, just needs a different wording to be clear.
My memory might trick me, but if I remember right, around the 4.19 era it worked as stated, nodes were evaluated 2 times for the example. Now they are only evaluated one time. (Which makes a huge difference, and as @ThiloN1987 mentioned, can lead to very hard to find bugs.)
I don’t mind how it works as long as it’s known - personally, I’d prefer no docs compared to a misleading one. I generally got to this issue after I wrote a blog post about it (https://medium.com/advanced-blueprin…s-516367cff14f), and in one of the comments they wrote that I stated it wrong, because the official docs says otherwise. I followed up on it, and now I believe the documentation might be the wrong (or misleading) one here.
Epic hired someone a few months ago: a contractor (so its not clear if this is just a temporary thing).
But they’re the only one responding to docs issues on the forums right now. So maybe they can help…
Hey @KristofMorva, I just looked at this, and here’s what’s going on. Pure nodes are limited to one execution per data-gathering phase. Or, if you prefer, per execution cycle. So when that Set node in your example is about to run, all the pure nodes connected to it (directly or indirectly) are executed. A node that’s connected multiple times will still only execute once. If you had multiple copies of that node, they would execute once each. And if you have one pure node that feeds into multiple Set nodes, each Set node would trigger a separate execution of the pure node. In other words, the case you’ve found there is the only case where the pure node will cache a result.
What’s great about this behavior is that it’s efficient on CPU cycles and, more importantly, it’s predictable. This is also why it’s so important that pure nodes don’t modify data. If Get Expensive Value modified data, like…let’s say it alternates outputting 0 and 1. Well, the Blueprint author would have no way of choosing, or even knowing, what value would reach Set. Is it “1 + (0 * 2)”, or is it “0 + (1 * 2)”? We don’t know, because the order in which UE4 checks those input wires will now affect what those wires carry. Because pure nodes explicitly do not control execution flow, there’s no inherent order to them. So back to the “don’t modify data” contract, the engine assumes you’ve kept the contract and therefore that subsequent runs of the same calculation on the same data should produce the same result. It therefore caches data in the one case where it knows this should be safe, which is when a single pure node is accessed multiple times without execution flow advancing.
I hope that helps to clear up what’s going on there. @echo_four and I are going to update the documentation to make it a little clearer in there, too. Thanks for bringing it up!
Thank you very much for looking into it; yes, I have also experienced the same mechanism. It’s great that you could confirm, I wasn’t sure if the documentation lacks this info or if I missed something
Updating the docs to be clearer on this topic would be great!
This is a great explanation! One last thing is needed to drive this home though. I’ve been reading a bit about this topic and it seems behavior might differ depending on whether the function is const or BlueprintPure or both, and also whether we’re before or after 4.22. In what configuration of those parameters would you say is applicable to your answer?
I’ve been marking my pure functions as const all along as it seemed sufficient, but I’m really re-thinking this. Could you speak to the different behaviors (in reference to your answer) for functions that are const only, BlueprintPure only, and both?