我正在使用Nashorn实现一些对性能敏感的代码。我是这样做的:
private ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine(new String[] { "--no-java" });
String someExpression = "someFunction() + someVariable";
// this compiled script gets cached, caching code omitted
CompiledScript script = ((Compilable)engine).compile(expr);
MyScriptContext context = new MyScriptContext();
Object output = script.eval(context);
在运行时,Nashorn坚持要对MyScriptContext进行大量必要的调用。它坚持在每次调用eval()时调用MyScriptContext.getBindings()。put(" nashorn.global",anObject)。然后调用MyScriptContext.getAttribute(" someVariable")(它应该)和对MyScriptContext.getAttribute的调用(" someFunction")(它不应该)。
它不应该调用" someFunction()"因为该函数在编译时可用。 " someFunction()"需要在编译时编译为字节码并绑定,而不是每次调用eval()。 eval()处于紧密循环中。
如何说服Nashorn减少对MyScriptContext的调用?
答案 0 :(得分:34)
Nashorn必须在上下文中查找每个全局定义的变量(包括全局定义的函数),因为全局变量可以在外部重新定义,并且没有办法知道它们没有被重新定义。因此,我们无法在字节码中提前绑定函数。我将概述几种提高性能的方法。
您可以通过在匿名函数中定义程序来提高性能,从而为其提供非全局范围:
(function() {
// put your original code here, like this:
// ...
function someFunction() { ... }
...
someFunction();
...
})();
在这种情况下,匿名函数内的函数对象最终可能存储在字节码局部变量中。
通常,如果您的代码对性能敏感,请尽量减少对globals的使用。如果需要使用全局变量,甚至可以将它们移动到函数的参数中,以便它们成为局部变量。例如。如果您的代码依赖于全局x
和y
,请执行:
(function(x, y) {
// put your original code here, like this:
// ...
function someFunction() { ... }
...
someFunction();
...
})(x, y);
显然,这仅适用于对变量的读访问。 (这适用于任何函数,当然,不仅仅是一个匿名的立即调用的函数表达式;它只是我所需要的一个构造,当我需要的是从全局词汇上下文转移到私有上下文时)。
实际上,你可以做得更好。在上面的例子中,你仍然会评估anon函数的主体,它将创建函数对象。 (这并不是那么糟糕,请注意;他们不会再次编译。函数对象本质上是一对指针:一个用于编码,一个用于词法范围并且可以快速创建。代码是编译一次。)但是如果你可以使你的anon函数的词法范围不可变,你可以只创建一次并从中返回一个函数,它将在其自己的范围内看到所有其他函数:
var program = (function() {
// put all your function declarations and other constants here
...
function someFunction() { ... }
...
return new function(x, y) {
// put your original code, minus the function declarations etc. here
...
someFunction();
...
}
})();
(此时,您甚至不必使用Java中的CompiledScript
,但我建议您在向发动机传达您希望为重复评估优化的表示的意图时这样做。
从Java开始,现在您可以执行script.eval()
后跟JSObject program = (JSObject)context.get("program")
,然后随program.call(null, x, y)
多次调用它。 (JSObject
是Nashorn面向Java的接口,用于本地对象,包括普通对象和普通对象)。
或者,您可以使用engine.compile("program(x, y)"
创建一个不同的脚本进行调用,并确保在x
之前将y
和eval()
放入上下文中。
通过这种方式重复评估,你会减少最多。但值得注意的是,所有调用都将共享最外层匿名调用的词法范围。这就是你如何获得相同的函数对象而不需要重新创建它们,但也要注意,如果你有任何可变状态(函数范围内有一些var
),他们就会这样做。也可以分享。