This is a no brainer, easy as that, outputs never use mod and do direct storage.

Second test:

Code Snippet

[SlicewiseMethod(Name ="Sin", Category ="Value")]

publicstaticvoid Sin(

[Input("Input")] double d,

[Output("Output")] outdouble dout)

{

dout =Math.Sin(d);

}

Here we have one input and one output. The compiler can detect that, and automatically set the input read to safely ignore the mod operator too.

Now let's look at our lerp above, and let's say that we want only one lerp value for all elements:

Code Snippet

[SlicewiseMethod(Name ="SimpleLerp2", Category ="Value")]

publicstaticvoid SimpleLerp(

[Input("Input 1")] double d1,

[Input("Input 2")] double d2,

[Input("Amount", IsSingle=true)] double amt,

[Output("Output")] outdouble result)

{

result =VMath.Lerp(d1, d2, amt);

}

Here we did set the IsSingle Attribute to our variable. Compiler will detect that, read the variable into a local once before the for loop.

Now let's say we want to multiply a spread by a constant value:

Code Snippet

[SlicewiseMethod(Name ="MultiplyFixed", Category ="Value")]

publicstaticvoid SimpleLerp(

[Input("Input")] double d,

[Input("Amount", IsSingle =true)] double amt,

[Output("Output")] outdouble result)

{

result = d * amt;

}

Here as before, our Amount variable will be stored into a local.
But now we also know that our Input Variable is alone (same as the Sine case).
So compiler will remove the mod operator on input value as well!

Now let's see a little bit more complex case:

Code Snippet

[Selector(Name ="Function", Category ="Value Inverse")]

publicstaticDictionary<string, Func<double, double>> TrigonometryInv

{

get

{

var result =newDictionary<string, Func<double, double>>();

result.Add("Asin", (x) =>Math.Asin(x));

result.Add("Acos", (x) =>Math.Acos(x));

result.Add("Atan", (x) =>Math.Atan(x));

return result;

}

}

Here we basically build a node that Has one input, one output , and and enum to select function.

In that case we simply ignore all the mod operators, and Mutable clearly wins.

X = 50000, Y = 1
Native : 0.36 ms
Zip : 0.25ms
Mutable : 0.16ms

We can also see that pushing a local and setting other at full speed gives a decent gain. So setting path at runtime is significant.

So technically, that gives us this information:

let function (p1,p2,p3,p4) = dosomething

Now what we have, to generalize our vector, is the following (for each parameter)
if length == 1 -> set local before loop and use that local
if length == SpreadMax -> read without mod
if length < SpreadMax -> read modded

Of course we can clearly see that we have a combinatorial explosion, a function with 8 parameters has a LOT of different cases.

Ask the runtime if we already have the function, if not compile it and give it back.

Run the function.

Yes, clearly as you've seen, we have nodes that self compile ;)

So this is of course to use with caution, if your node input length vary a lot, you might have a lot of recompile.
If you have reasonably steady spread counts, you'll have a little overhead on startup. Please note that something like:

let function (a,b) => a+b
a = 2000, b = 1
next frame , a = 1000, b= 1

will not cause a recompile, since a is is still at SpreadMax.

On Some specific notes, node compiler is about 0.5 millisecond, which is pretty fast, but if you have 1000 nodes rebuilding on the fly, that's not too good ;)

What is good is that our node is now a basic container, so we save some il code. Second very cool thing, our logic becomes a delegate, which is much easier to wrap into a Task, but more on that later.