Event sheet JS script for getting a vector normal running more slowly than event sheet based method?

0 favourites
  • 9 posts
From the Asset Store
Full game Construct 2 and Construct 3 to post on Google Play
  • Hey all,

    I have a construct function for calculating the normal of a vector.

    I made an equivalent version in a js script and imported it. Here is the JS:

    export var x = 0;
    export var y = 0;
    
    export function Normalize (ax , ay) {
    	
    	var length = Math.sqrt(ax * ax + ay * ay);
    	
    	 if (length === 0) {
    	 	x = 1;
    		y = 0;
    	 } 
    	 else {
    	 	x = ax / length;
    		y = ay / length;
    	 }
    	
    }

    I should add that the eventsheet based method is optimized to use as few events as possible and uses expressions like set Variable x: length = 0? 1 : ax/length

    If I run Normalize(1,1) in a js block on an event "repeat" 1000 times, the cpu is running at around 22%, nothing else is going on.

    If I run the version I created in event sheet through functions, I get around 30% cpu. If I inline the function and eliminate the function call, I get around 8%.

    Basically, the eventsheet function call is ridiculously costly, which is already known, but what I don't understand is why the javascript version is performing worse that the event based one. Is there an "overhead" to a javascript block running that is junking up the performance when used with eventsheet loops?

    If I take away the eventsheet loop condition and replace it with a for loop in JS, the cpu time goes down to 3%, which is basically nothing. What am I missing here?

  • Read Optimisation: don't waste your time. Questions like this are generally pointless. Just make your project ignoring performance. If it is not fast enough by the end, then you can spend time optimising it, but there's a good chance it will be fast enough anyway. JavaScript is extraordinarily fast and you can probably just trust it to be fast enough.

    Performance testing is hard, and your test is an unfair one:

    If I run Normalize(1,1) in a js block on an event "repeat" 1000 times, the cpu is running at around 22%, nothing else is going on.

    JavaScript JIT optimisers are extremely sophisticated these days. If you run Normalize(1,1) repeatedly, the optimiser may well notice "well, that calculates to (0.70710678118, 0.70710678118), and so I'll just replace the call with the answer". Then depending on your benchmark it might figure out that you don't actually use the answer, and so completely delete the code. So now your performance benchmark is doing nothing, and is just the overhead of the "repeat".

    Add in to that the fact the timer-based CPU and GPU measurements are subject to hardware power management which makes them misleading at low readings, and it can be hard to tell if something is actually faster or not. Tests like this are only meaningful if you run them at full capacity and use an FPS reading to eliminate the effect of power management.

    Your other tests may or may not actually calculate a normalization depending on how the code is run. In other words tests like these are usually meaningless and will just confuse and mislead you, unless you have good understanding of the JIT and take care to ensure it actually runs the code you intend.

  • Ashley

    I wrote such a long reply but I was logged out when I hit reply.

    TLDR: I fixed the test. I also made a few more.

    I am not arbitrary in these. I have had to move projects from construct to unity in the past due to performance reasons. Every construct project I have ever worked on beyond prototype has had performance bottlenecks. I am not simply making random tests for fun, but creating small prototypes that help me determine if c3 is the right engine for this project. I can achieve 80fps in unity with 100,000 objects normalizing a vector projection for movement.

    1000 was chosen because that is my design goal and is reasonable. But so for construct is unable to meet my desired performance goals.

    The issue is this: (JS blocks have a surprising overhead, functions unsurprisingly have overhead, and must be inlined when used frequently)

    JS blocks have significant overhead even if they are empty and do nothing. If a loop is being used to iterate through objects, it is best not to use JS blocks as actions work faster. This is a confusing result. I changed the JS so it would iterate through an array of data and calculate the results to another. So long as no loop was used in the event sheet, the JS ran super fast. So there is simply an overhead to each JS block that is accentuated in an eventsheet loop.

    I knew built in eventsheet functions had an overhead, and would perform worse... blah blah blah...

    But the fact is, I am manually inlining functions in order to achieve acceptable performance, and that is NEVER a good thing.

  • The issue is this: (JS blocks have a surprising overhead, functions unsurprisingly have overhead, and must be inlined when used frequently)

    The overhead to call a JS block in the event sheet is: "call an async function". So this does not really make technical sense. It should be a minimal overhead. Hence my assumption the benchmark was wrong (especially since you talked about doing things which would qualify as an unfair test, as a good JIT would replace or delete the code you are running).

    If you have a concern about performance, it is 10x more helpful to share an actual project file demonstrating a realistic situation, than just talking about it.

  • drive.google.com/file/d/15kPU45uWAiLnM5aEOeMbDIgXqXpkL5e3/view

    I simply disable any events I don't want to run. and then look at the global variable timetocomplete to get the duration the test took. The majority of events to toggle are in the repeat loop. The loop runs one million times. A global variable is set to 50, and is the number of times the test runs. I probably made some mistake in counting so it might actually be 49 million times the test runs... or 51 lol.

    As per my other related post that you linked to, I am not arbitrarily inventing performance test to run, just for fun. I am running these because they are anticipated sticking points relevant to my project. You say "just make the game and worry about this later"? That's easy to say when you haven't already undergone the pain/time of porting projects to another engine multiple times because it wouldn't work in one or the other. Obviously you know how much work it took to change the engine from c++ to js. It pays in time and money to make sure you can feasibly create your project in the workspace and I am really limited on the former.

    Anyway, I think this brings up two major considerations for me, and any project:

    1. Calling a JS block is, for some reason, incredibly expensive, compared to basic event sheet ACES. This means that there is no reason to replace actions with JS, unless you are using a single JS block to replace many aces. Idealy, JS should be used to minimize event sheet loops and should never be used in an eventsheet loop.

    Obviously, All of this is only applicable when working with a large number of objects.... say more than a few hundreds, like my original tests were geared for and what my game is going to call for.

    But this really highlights the idea that you can use JS blocks to improve performance.

    2. Event sheet functions still are way bad for performance if you are structuring your project around them in a sensible and reasonable way... for example: Don't repeat yourself...

    I need a vector/math library for my game. I made a prototype project that uses event sheet function calls for this. They take 4 parameters (ax,ay,bx,by) and set globalVarables to handle multiple returns (rx,ry). Anyway, it isn't unreasonable to call a function from a function from a function... Function GetProjection is going to call function Normalize 2 times. If I really practice DRY, Normalize is going to call a function to get the length squared of a vector and then get the sqrt of that vector. TLDR, a simple function call to a library like this can easily call 6 other functions to do its job. You get 100 objects all wanting to get a vector projection, and all of a sudden you have 600 function calls and now you have already chewed through a decent chunk of your available cpu budget, and thats before you call functions for all those objects, like "seek" "arrive" and "avoid" which all will involve yet more vector math.

    JS seems like a good option here, but simply replacing event sheet function calls like normalize() with a JS blocks calling a module to do the same thing.... performance isn't particularly better. Its WAY better to take that library and stuff it in a plugin and use the plugin instead - because then at least it feels like the JS is married to the ACES, but this goes back to behaviors and plugins being tedious to make and having to write alot of stuff just to make one ACE. (Skymens plugin builder is amazing in this regard - As an aside, if I recall, one of the things that the c2 community really was hyped on for c3 was basically to be able to build extensible behaviors and package them up and share them, all from within construct, but it seems that never happened for whatever reason).

    Otherwise, the only other option is manually inline your function calls. That is tedious, unscalable, poor design, but saves a huge amount of performance where functions call other functions as in the example above.

  • Try Construct 3

    Develop games in your browser. Powerful, performant & highly capable.

    Try Now Construct 3 users don't see these ads
  • look at the global variable timetocomplete to get the duration the test took.

    Your project does not display this variable. Are you looking in the debugger? Because debuggers have performance overhead. And so then your test is basically measuring the debugger overhead. Normal preview is faster.

    for (i = 0; i<=1000000; i++)
    	runtime.globalVars.NumberA = 
    		Math.sqrt(runtime.random(100) + runtime.random(100));
    

    Code like this is still an unfair test: an optimizer may be able to identify that the same value is overwritten 1000000 times, so it can skip the first 999999 calls; then since the 'i' variable is unreferenced, it is OK replace it with no loop and just one assignment. I don't know if it does or doesn't do that optimization, but in theory it can, so you shouldn't write a test like that. Usually at least summing a number every iteration and then displaying the result (so the sum is not dead-code eliminated) is a better approach.

    I profiled the empty JS block case. Given an empty loop does nothing, then adding something in to the loop - even an empty async function call - means you're measuring nothing vs. something, so the numbers will be big. I don't think that by itself means the result is meaningful. Still, it seems to spend most of its time in GC; perhaps the fact it's an async function call means it has some GC overhead and so you end up measuring that.

    I don't think that's a sensible thing to do if performance is important though. Crossing interfaces always has a performance cost. For example in some C# engines, calling C++ code is expensive. So if every time you call a math function in C# it has to jump in to C++ code, and you benchmark it, it'll be slow as it's all overhead for jumping between interfaces. The same thing is happening here. So high-performance engines tend to do all their math stuff in C# so there is no overhead and only call in to C++ for more expensive things (which then have all their own C++ math functions, so there's no need to jump back to C# for that).

    The obvious thing to do is if you need maximum performance, write the loop itself in JavaScript, and avoid mixing interfaces (in this case event blocks and JS). So why not just do that?

  • The obvious thing to do is if you need maximum performance, write the loop itself in JavaScript, and avoid mixing interfaces (in this case event blocks and JS). So why not just do that?

    I did that too, which I mentioned and is in the project, to compare and point out that was blazing fast. The TLDR was don't use JS blocks inside of big eventsheet loops, or eventsheet blocks that will get called alot, which I had been doing in my project and wondering why it was so slow.

    The reason you can't write the loop in JS is because you might be doing other things in that loop that you can't handle with JS, or should by recommendation of the SDK manual.

    But this isn't expressed in the manual, and everybody is like "Performance problems?" write a JS module or script. But it turns out this doesn't solve the issue at all, but makes it worse.

    Thus the only solution is AddonBehavior with sdk (tedious and laborious to write) or built in event sheet functions, but they have the overhead too.

    Which, leads me back to the issue about the insanity of manually in-lining construct functions in order to improve performance. Which is BAD for scalability, maintainability and general mental well being lol.

  • Also, to add, I have the other test where I mentioned you can read out the results to an array and verify the loop is running.

    And yes, I used the debugger in this case, and I know it has overhead. Are the results accurate? No. But I think they are good enough, and certainly mirror what I have been seeing in my project:

    Pure JS = fastest

    Pure Event sheet no functions (function inlined) = next best

    Eventsheet with c3 event functions = okay

    JS in JS blocks mixed in eventsheet = Awful

    I mean it all depends on what you are doing, and I am doing 1000 objects, which imo isn't much, but already requires careful attention to this stuff.

  • If you need maximum performance at all other costs, choose pure JS then. Surely you can have the performance sensitive parts in pure JS and the general purpose stuff in event sheets or mixed event blocks/JS.

Jump to:
Active Users
There are 1 visitors browsing this topic (0 users and 1 guests)