V8ScriptEngine.Compile vs. V8ScriptEngine.Script

Aug 6, 2013 at 1:11 AM
I'm trying to wrap my brain around the advantages of compiling a script with V8ScriptEngine.Compile versus dynamically evaluating script code using V8ScriptEngine.Script.

Suppose I have script code like this:
string code = @"
    function computeFoo(foo)
    {
        return (foo.inputA * foo.inputB);
    }
";
In my C# code, I have a class Foo defined as follows:
class Foo
{
    public double inputA { get; set; }
    public double inputB { get; set; }
}
Now, suppose I need to "computeFoo" in a loop over and over again. It seems like I have two choices: dynamic evaluation with the V8ScriptEngine.Script member...
V8ScriptEngine engine = new V8ScriptEngine();
engine.Execute("DynamicJS", false, code);
for (int i = 0; i < 50000; i++)
{
    Foo foo = getFoo(i); // this function returns an instance of Foo
    var result = engine.Script.computeFoo(foo);
    // do something here with result
}
...or I could compile the script code and re-execute the compiled code over and over. But if I do that, it doesn't seem that I can pass any parameters to the "computeFoo" method in my script code. Instead, it seems that I have to do something like this:
string code = @"
    var foo = hostFoo;
    function computeFoo()
    {
        return (foo.inputA * foo.inputB);
    }
    computeFoo();
";
V8ScriptEngine engine = new V8ScriptEngine();
Foo foo = new Foo();
engine.AddHostObject("hostFoo", foo);
V8Script compiledCode = engine.Compile(code);
for (int i = 0; i < 50000; i++)
{
    foo = getFoo(i);
    var result = engine.Evaluate(compiledCode);
    // do something here with result
}
I've done some timing tests, and while the compiled approach is faster, it's not as dramatically faster as I had expected (it's more on the order of 20-30% faster). And if I make the "Foo" class a .NET class such as a Dictionary<>, the speed differential between the compiled and dynamic evaluation approach is narrowed even further.

Considering that parameter passing from C# to the script code is less elegant with the compiled approach vs. using the Script member, I'm almost tempted to ditch compilation. The script code is something that is provided by our customers, and it's much more intuitive to tell them to create a "computeFoo" function that looks something like the first example versus the second one. Or am I missing something fundamental about how to pass parameters to compiled scripts?

Thanks!
Coordinator
Aug 6, 2013 at 4:38 AM
Hello sgammans!

Your first technique above (the one that goes through ScriptEngine.Script) is the right way to invoke an existing script function multiple times. We'd actually recommend caching a reference to the function:
V8ScriptEngine engine = new V8ScriptEngine();
engine.Execute(code);
dynamic computeFoo = engine.Script.computeFoo;
for (int i = 0; i < 50000; i++)
{
    var result = computeFoo(getFoo(i));
    // do something here with result
}
Your second code sample executes the same function statement over and over, needlessly redeclaring the function 50000 times. In addition to making it difficult to pass arguments, this could create 50000 copies of the function for the garbage collector to clean up. We say could because V8 might be smart enough to optimize the duplicate functions away.

Frankly we're surprised that the second technique is faster. Our suspicion is that this is because (a) computeFoo() is trivial, and (b) V8 is very good at avoiding duplicate work.

By the way, in your second code sample, the line foo = getFoo(i); only reassigns the .NET variable foo; it does not affect the script variable hostFoo. If we're reading your code correctly, this means that computeFoo() will operate on the same Foo instance every time. The code should probably be something like this instead:
V8ScriptEngine engine = new V8ScriptEngine();
// Foo foo = new Foo();
// engine.AddHostObject("hostFoo", foo);
V8Script compiledCode = engine.Compile(code);
for (int i = 0; i < 50000; i++)
{
    // foo = getFoo(i);
    engine.Script.hostFoo = getFoo(i);
    var result = engine.Evaluate(compiledCode);
    // do something here with result
}
But again, the other way is much better. It's simpler and cleaner, and it should be faster and more efficient in real-world situations.

If you're wondering then what the point of compilation is, consider a scenario where your client provides arbitrary script code (not a single script function) that you must execute many times. In this situation, the following:
void ExecuteManyTimes(V8ScriptEngine engine, string code, int repeatCount)
{
    using (var compiledCode = engine.Compile(code))
    {
        for (var i = 0; i < repeatCount; i++)
        {
            engine.Evaluate(compiledCode);
        }
    }
}
is much faster than this:
void ExecuteManyTimes(V8ScriptEngine engine, string code, int repeatCount)
{
    for (var i = 0; i < repeatCount; i++)
    {
        engine.Evaluate(code);
    }
}
In addition, compiled scripts can be shared among multiple V8ScriptEngine instances that share a single V8Runtime.

Cheers!
Aug 6, 2013 at 4:14 PM
Wow, I'm glad I asked then, because my test results so far are that re-evaluation of compiled code is faster than dynamic evaluation using the Script object, and coming from a C/C++/C# background I just intuitively started from the position that re-executing "compiled" code would not mean that I was redeclaring that code on each pass. Thanks...
Coordinator
Aug 6, 2013 at 5:58 PM
Actually, on second thought, roughly speaking they should be equally fast, as both amount to re-execution of compiled code. The second technique does redeclare the function; that is, it reassigns a global script property. But it probably doesn't duplicate the function, so the redeclaration is effectively a no-op.

However, note that the first technique correctly passes an argument, whereas the second one doesn't, due to the bug described above. Exporting a .NET object to the script engine is not cost-free. Also, the first technique redundantly imports the function from the script engine, and that also is not cost-free. Together these factors might add up to the discrepancy you're seeing. Would it be possible to make the recommended changes (cache the script function in the first case, and fix the argument passing in the second) and rerun the timing tests?

We'd like to reiterate that in both cases you're compiling code once and re-executing it many times. The two techniques have minor differences, but both should be much faster than recompiling the code many times.
Aug 7, 2013 at 10:50 PM
The difference in average runtimes between the two techniques is now so small as to be negligible. That's good, because using the V8ScriptEngine.Script object to invoke the JavaScript method is infinitely preferable. Thanks!