【问题标题】:Nashorn inefficiency纳肖恩效率低下
【发布时间】:2015-02-14 03:27:51
【问题描述】:

我正在使用 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 的调用?

【问题讨论】:

  • “我正在使用 Nashorn 实现一些对性能敏感的代码”你为什么要用脚本语言来实现对性能敏感的东西?
  • 因为我必须这样做。这是一个基本的应用程序要求。是的,在这个应用程序的上下文中它是有意义的。

标签: java-8 nashorn


【解决方案1】:

Nashorn 必须在上下文中查找每个全局定义的变量(包括全局定义的函数),因为全局变量可以在外部重新定义,并且无法知道它们没有被重新定义。因此,我们永远无法在字节码中提前绑定函数。我将概述几种提高性能的方法。

将您的代码包装在立即调用的匿名函数表达式中

您可以通过在匿名函数中定义程序来提高性能,从而给它一个非全局范围:

(function() {
    // put your original code here, like this:
    // ...
    function someFunction() { ... }
    ...
    someFunction();
    ...
})();

在这种情况下,匿名函数内的函数对象最终会被存储在字节码局部变量中。

通过将全局变量作为参数传入来减少对全局变量的依赖

一般来说,如果您的代码对性能敏感,请尽量减少对全局变量的使用。如果您需要使用 globals ,您甚至可以将它们移动到函数的参数中,以便它们成为那里的局部变量。例如。如果您的代码依赖于全局变量 xy,请执行以下操作:

(function(x, y) {
    // put your original code here, like this:
    // ...
    function someFunction() { ... }
    ...
    someFunction();
    ...
})(x, y);

显然,这仅适用于对变量的读取访问。 (当然,这适用于任何函数,而不仅仅是匿名立即调用的函数表达式;它只是我在需要从全局词法上下文移动到私有词法上下文时使用的构造。

使用外部匿名函数来保存代码和另一个用于评估

其实,你可以做得更好。在上面的示例中,您仍将评估 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)" 创建一个不同的脚本进行调用,并确保在eval() 之前将xy 放入上下文中。

这样可以最大程度地减少重复评估。不过需要注意的重要一点是,所有调用都将共享最外层匿名调用的词法范围。这就是您无需重新创建即可获得相同函数对象的方式,但也要注意,如果您有任何可变状态(函数范围内的一些 vars)它们也会被共享。

【讨论】:

  • 这是一个出色的答案。你应该把它写成一篇博文。
  • @ccleve,您对提供的答案发表评论,我同意您的看法,这是一个不错的答案。但更重要的是,您是否实施了他的建议,如果是,它们是否会实现绩效提升?请评论或更新您关于改进的问题。谢谢!
  • 是否有适用于 Nashorn 的基准?我们正在尝试运行一个由 Webpack 构建的 js 包,而 Nashorn 似乎比 Rhino 慢(因为包太大而在解释模式下运行)慢了 50%。这是真的吗,还是我们遗漏了什么?
  • @Jeach 是的,他们做到了,对我们来说执行速度提高了 3-4 倍(同一脚本的 10000 次串行执行)