Multi-output Pure functions - Inefficient?

I have been using pure functions for a while. I think its nice to be able to mark your functions as pure to let other people using your library know that they are pure get operations. However in doing a pass on optimizing a blueprint I’m working on I recalled this:

Source: Functions | Unreal Engine Documentation

Does that mean that when you have a multi-output pure function and you read the outputs using different nodes, the function will be executed multiple times? From the description I would clearly think so. But if thats the case I would think it strange that there are pure blueprint functions as part of the engine that are relatively computationally expensive, have multiple outputs and are still marked pure. Take this for example:

ConvertScreenLocationToWorldSpace is not the most expensive operation I admit, but executing it more than once in this situation would be unnecessary. Does anyone know how often this would be executed if execution reaches the branch node? Once (ideal), twice because of two execution nodes, or thrice because thats the number of nodes it is connected to?

It’s a trap! Epic calls them ‘pure’ nodes but they are not pure. Most of the nodes marked as ‘pure’ are actually only const.
const functions are most of the time not pure because they may rely on local state (class state).

Your node is executed at least two times. I’m pretty sure it is three times, but I would need to check the source…
The reason is that, due to the node depending on local state, it’s result may change. E.g. when you move your camera after the branch node, the node would produce a different result when printing.

I never considered pure to be something special, since from my experience you can easily create functions that do change the object’s state and mark it pure (in both C++ and BP). Its definitely looser than const.

But I still think that when used wisely marking functions as pure can make an elegant interface when you’re making blueprints for use by other people. If the graph I showed is executed two or three times though I don’t think its worth the extra computational cost.

Purity has its origin in functional programming languages. It is a promise: Given a certain input the function will return always the same output. (No side effects)
There are some comments in the UE code that indicate that Epic actually wanted to implement it that way.

// @todo: the presence of const and one or more outputs does not guarantee that there are
// no side effects. On GCC and clang we could use __attribure__((pure)) or __attribute__((const))
// or we could just rely on the use marking things BlueprintPure. Either way, checking the C++
// const identifier to determine purity is not desirable. We should remove the following logic:

Why is that important?
If implemented correctly you can optimize. Say a pure function is called every thread with the same input. Since we know that the output will always be the same we can cache the output and save performance.
We could multithread the function calls without worries - probably not worth it for now but may be interesting in the future.

I love blueprint because they help you visualize the flow of state. Say you have the following code in C++.

FVector Pos = GetActorLocation();
SetActorLocation(Pos + Offset);

Reading the code we don’t know if it is important that SetActorLocation is called before DoSomethingWithTheActor.

Now compare

The second variant implies that DoSomething does not care about the changed actor location and may be put before SetActorLocation

But ‘pure’ functions destroy the picture
There is no visual hint that the GetActorLocation node result may change during the execution path. Now imagine this example with more nodes or the SetActorLocation call inside a nested function call.
You actually need to add a local variable or wrap the GetActorLocation inside another function to fix this
or this one: I hate the make array node :frowning:

Whenever you declare a const function as pure you introduce the possibility for a (semantic) bug. They may be useful when you work with data that may not change during gameplay. e.g. the game mode or player controller. But please don’t declare const functions as pure if you expect their dependent state to change (positions, variables etc.)

Now this is my opinion having a functional programming background. I am very interested in yours. Did you stumble upon the same bugs as I described? Can you show me examples where it results in a clear interface?

I wasn’t aware that purity was a concept borrowed from functional programming, thats good to know. And while I suspected pure functions can allow some compiler optimization, I also thought that with the current ability to mark every blueprint function as pure its unlikely that optimization is already in. I think it isn’t.

Technically, the self reference points to an object that has been changed so it does not break the promise that given the same input (which has changed), it will produce the same output. The array example gave me a headache, I assume it creates two arrays, one for the Add node and a new one for the Length node. I haven’t stumbled on bugs like that before, but thats because I purposely avoid having graphs like the ones you’ve showed. In the SetActorLocation case, I would have copied the GetActorLocation node for the second call to make it clear to any readers that it will be a different value. All in all I get your point that pure nodes (when used unwisely) can cause confusion.

Let me clarify in case we’re talking about different types of interfaces. I’m working on a blueprint library that has a lot of blueprint getter functions to retrieve data. When I say clear interface I’m concerned about whether the functions appear like people would expect them. Since a lot of the default getter-type nodes in the engine appear as pure nodes, I find it nice that we can create these types of nodes as well so that the functions we want to make accessible to others appear consistent with the default engine nodes. So I’m just happy that my nodes appear consistent with the default nodes, I think that makes it clear for the user because for getter-type functions he would expect a pure node.

However, my dilemma now is whether to keep using pure nodes even when they follow Epic’s rule to “not modify state or the members of the class in any way”, because of the inefficiency if those nodes MUST be executed multiple times when reading multiple output in subsequent steps.

Purity and pointer to objects don’t really work together. I too started to copy all getters for every use. There is still a problem with transformations. Say you take the actor location and do fancy stuff with it, like here: looking at target object while rotating around it - Blueprint Visual Scripting - Unreal Engine Forums
You would have to copy all nodes up to the pin that is linked twice, or use local variables.

(Note: At this point I’m writing for future reference :slight_smile: back to your problem)

For my cases I introduced a “Snapshot” node (or multiple). Snapshot meaning taking the value at a certain point in time. e.g. GetActorLocationSnapshot or SnapshotActorLocation. This snapshot function just calls the pure node inside. You can then choose to use GetActorLocation or SnapshotActorLocation depending on requirements. If users of your library encounter low performance problems you can point them to the snapshot nodes.

There is still the problem that the getter node with three output pins will be called three times. But with the snapshot node you can turn that into a maximum of three times.

>snapshot function calls
That’s actually super-clever as a way to guarantee that a Get is called at a specific point in an execution path. I may use that in the future; beats the heck out of making separate variables for interpolating.

It is a pretty good idea and it does save us some one-off trivial variables.

Just wanted to know this too. From my little test with a custom pure function that has a PrintString inside of its body, it looks like it is only called once as long as you only access all the return values no more than once (so, no > 1 wire connected to any return value).