Balancing C++ and Blueprints

Started by
13 comments, last by Juliean 1 year, 6 months ago

@MagForceSeven this is also great to hear. I spent alot of classes during my degree working in OOP's like Java, in addition to C# and C++, so the appeal of having full control over your classes and their parameters, as well as being able to pretty easily view polymorphism when used, is something I had considered when learning how to implement C++ in UE. I think the biggest hurdle, as of now, is that using C++ in UE pretty much requires that I'm actively looking at the API until I get really comfortable with how code integrates with the engine itself. There is a lot of engine specific syntax/jargon that I think will really just get easier over time.

I've known that BP is slower running, and especially in larger projects can cause performance issues if used en-masse. I think the appeal for using it for some front-end stuff is certainly still there, and even for rapid prototyping of ideas it seems pretty great, as its easy to write quickly and test without needing to recompile everything and restart the engine (I know live-coding is a thing in UE5, but thats besides the point).

I appreciate that this topic has generated a pretty solid back and forth, and validates what I had originally thought…..this is a complex topic! I'm hoping that as I get more exposure to C++ in Unreal, things become more natural, but as of now, it certainly feels like I'm relying pretty heavily on the API as I make things.

Advertisement

frob said:
Not easily, no. It's quite difficult, in fact. There have been attempts at nativization tools in the past, and there are tools and scripting languages like Anglescript that can help with some of the concerns, but overall there is a very poor mapping from the visual elements to the actual executable code. There are many cases, especially error cases and visual script pieces that are just left floating out there which cause severe problems with the system that is running it. So you're either faced with what they're doing today (interpreting it with a heavy runtime library that validates and corrects as it goes) or you're facing a model with a more fragile model that requires designers, artists, and others to write code like programmers do with the same rules that programmers dedicate years of their lives studying followed by tremendous work time in validating and resolving.

Do you have any examples on what those problematic pieces are? I do find it hard to belive, after all visual scripting is just a visual interface for a programming-language, with a lot of the same features that their C++ has. The main additions that I can think of are “delay” nodes and the like, but those are implemented as “tasks” as far as I can tell. My perspective is definately biased towards using blueprints as a competent programmer, though I really fail to see what people could do that would prevent optimizing the blueprint-backend (just like its not possible for unexperienced people to write c++ that cannot be compiled to machine-code; as long as it can be compiled at all without source-errors. Or, maybe a better example, people cannot write C#-code that cannot be JIT-compiled and thus force the use of an interpreter).

Also, to be more clear, I'm not talking about their current nativization-tool per se, which from my understanding creates c++-classes out of blueprints that are then compiled like C++. I'm talking about a simple JIT, that takes the bytecode and turns it into some form of machine-code. My own blueprint-variant also has features that would make it impossible to create regular c++ out of it, but what I did do is simply turn all the stream of tokens/bytecode into machine-code directly, instead. So instead of having to do:

const auto opcode = readOpcode(bytecode);

switch (opcode)
{
case XXX:
 // do X
    break;
case YYY:
	const auto data = readData<int>(bytecode);
// do Y
	break;
} 

for a bytecode of “XXX→YYY”, you would create machine-code that does nothing more than place the code inside the switch-cases after each other in a machine-code segment. This does not make everything as fast as native code, but it removes the entire overhead of interpretation; as well as a few upsides (constants are actually constant in the machine-code; all CALLs are direct to a know address; etc…). I don't see how this could ever not be possible with blueprints. In the end, the individual statements inside an interpreter are also machine-code, just with a mechanism around it to parse tokens from a non-machine-code source.

Juliean said:
Do you have any examples on what those problematic pieces are? I do find it hard to belive, after all visual scripting is just a visual interface for a programming-language, with a lot of the same features that their C++ has.

I think the best parallel is asking “which raindrop is responsible for the flood?”

Individually every call, every function, every blueprint node has a cost. Individually the blueprint cost is relatively small, anywhere on the order from a few hundred nanoseconds to a few microseconds. Copies of UStructs are made all the time, and they're not free with allocations and duplication. Memory management for temporaries isn't free. They take CPU cycles, they consume cache space, they incur costs out to main memory. They're not inherently bad, but they accumulate.

The biggest issue I've had in restructuring designer-created code is their massive overreliance on iterating through containers, or doing them in bad ways. A recent example, iterating through several thousand objects cast them to the desired type, and proceed if they're a match. An individual cast takes just under a microsecond each. They were talking a huge chunk of each update merely trying to figure out if an object was the right one to continue processing.

As I mentioned earlier, designers often pull out naïve implementations that programmers are taught to avoid, like exponential growth or factorial growth functions. Other times it is ignorance of the cost of functions, ignorance about calling blocking functions, ignorance about the underlying details. This can be addressed by education and training, but at some point your effectively giving them a few semesters of computer science education.

Blueprints enable more people to be productive, and allow high-level changes with little developer work. They are quite powerful, and when leveraged effectively can provide a multiplying effect on what gets accomplished. They're a great tool when used well. Unfortunately they're also a performance nightmare when used badly, and unless someone is monitoring and measuring as changes are implemented, it's easy for designers and producers to create terrible performance which is difficult to pin down to a specific bit of blueprint.

frob said:
I think the best parallel is asking “which raindrop is responsible for the flood?” Individually every call, every function, every blueprint node has a cost. Individually the blueprint cost is relatively small, anywhere on the order from a few hundred nanoseconds to a few microseconds. Copies of UStructs are made all the time, and they're not free with allocations and duplication. Memory management for temporaries isn't free. They take CPU cycles, they consume cache space, they incur costs out to main memory. They're not inherently bad, but they accumulate.

Thats not quite what I was asking about though. “Every call” doesn't prevent them from using a JIT. “Every call” doesn't make nativization fail. Sure, in an interpreted language everything has an additional overhead, but writing Blueprint-Code with calls that exist in C++ should mean that a more optimal form of execution must be available. I was more interested in example for what prevents this or makes it more difficult, as you claim.

frob said:
The biggest issue I've had in restructuring designer-created code is their massive overreliance on iterating through containers, or doing them in bad ways. A recent example, iterating through several thousand objects cast them to the desired type, and proceed if they're a match. An individual cast takes just under a microsecond each. They were talking a huge chunk of each update merely trying to figure out if an object was the right one to continue processing.

So, part of the problem with blueprints being used by designers are bad algorithmical choices with high order of magnitude of iterations. I guess that at least makes sense, and I'm assuming in some cases we are talking about things that are so badly written that even if they were run natively it wouldn't be viable in production?

Though this again is not a point that should prevent them from providing a (relatively) more effective backend with a lower cost. One can write bad algorithms in C# without having to double-down with the code not being executed natively. I'm not sure if I'm missing something, or if I failed to communicate what I meant effectively. So in order to show what I mean, I've made a quick test both in Unreals and my own “blueprints”.

This takes about 0.534 seconds to execute, pretty bad for such a small loop, can't even use higher indices or it will detect an infinite-loop.

This is the same code in my own engine. Expect I do profiling externally, but the main overhead here should be the loop anyway. This function. compiled with a JIT, takes 0.011s. That is almost x50 faster! That is my point. There is no reason why a visual programming language needs to be that fucking slow. The same things about how inexperienced artists use it, would apply to my language that would to blueprints. But, without forcing a (apparently) broken nativization-tool, but simply by using (even a very dumb) JIT-compiler, it would mean that if you had an example where an arist made a loop over 1000 elements in UE-Blueprint and it was too slow, with a better backend it could have been almost neglible.

Does that make my point more clear? Obviously there is something making Blueprints slow, but it has nothing to do with the way that the user-interface is presented in the first place, because then my own example would be equally as slow, which it isn't even remotely close to.

EDIT:
For further reference, the same code in C++ (in my engine) performs the follow:

core::Timer	timer;
int x = 0;
for (int i = 0; i < 200000; i++)
{
	x = x + sys::Random::Max(1);
}

const auto duration = timer.Duration();
core::Log::OutInfoFmt("Took {}s", duration);

return x;

Debug: 0.025s
Release: 0.005s

So, my JIT outperforms Debug-c++ code by a factor of 2x at that point (probably since it uses a more optimal format for the generated ASM, which is more akin to how an optimizer would), while the optimized c++-code is about 2x faster than my JIT (with the difference being 5x between debug-release in this example). This should give a reference for the relative performances on my system, as well as further illustrate the point that blueprint is not bound to be slow by the fact of how its interfaced, but how its backend is integrated.

This topic is closed to new replies.

Advertisement