목공책 하나 들이셔요~

2014년 10월 10일 금요일

Java Scripting API 둘러보기 #4

이 글은 Java 6를 기준으로 쓰여진 Jeff Friesen의 "Taming Mustang, Part 2: Scripting API Tour"를 번역하고 Java 8에 맞게 수정한 것입니다. 원문은 다음을 참고하세요.

http://www.informit.com/articles/article.aspx?p=696621

Compilable 인터페이스로 실행속도 향상시키기

일반적으로 스크립트 엔진은 스크립트를 인터프리터(interpreter) 방식으로 실행합니다. 인터프리터는 보통 스크립트를 파싱하여 중간코드(intermediate code)로 변환하고, 뒷단의 실행 모듈이 이 중간코드를 실행합니다. 동일한 스크립트를 실행할 때마다 파싱하여 중간코드로 변환하는 과정이 반복되어야 하기 때문에, 스크립트의 실행속도가 느립니다.



만일 스크립트 엔진이 변환한 중간코드를 나중을 위해 저장해두고 재활용 한다면 성능을 크게 개선할 수 있습니다. 이를 가능하게 하기 위해 Scripting API는 Compilable 인터페이스와 CompiledScript라는 클래스를 제공합니다.

Compilable 인터페이스는 스크립트를 파싱하여 중간코드로 만드는 두개의 compile() 메쏘드를 제공합니다. 만들어진 중간코드는 CompiledScript 객체로 리턴됩니다. CompiledScript 클래스는 이 중간코드를 실행하는 세개의 eval() 메쏘드를 제공합니다. 당연히 이 eval() 메쏘드들은 스크립트를 파싱하여 중간코드를 만드는 과정은 수행하지 않습니다.

이러한 컴파일 기능을 제공하는 스크립트 엔진은 Compilable 인터페이스를 구현합니다. 모든 스크립트 엔진이 컴파일 기능을 제공하는 것이 아니기 때문에 컴파일 기능을 이용하기 전에 해당 엔진이 Compilable을 구현했는지 체크하는 과정이 필요합니다. 다음과 같은 코드를 이용하면 됩니다.

ScriptEngine engine = ....;
Compilable compilable = null;
if (engine instanceof Compilable)
    compilable = (Compilable) engine;

컴파일의 효과를 체감하기 위해서 스크립트를 파싱하여 중간코드를 만들고 이를 실행하는 과정을 수만번 반복하는 예제를 만듭니다. 같은 스크립트를 미리 컴파일한 다음 중간코드의 실행만 역시 같은 횟수로 실행하여 이 둘의 실행시간 차이를 비교해 보았습니다. 아래 코드를 보십시요.

// ScriptDemo6.java
import java.io.*;
import javax.script.*;

public class ScriptDemo6 {
    final static int ITER_MAX = 50000;

    public static void main(String[] args) throws ScriptException {
        // Create a ScriptEngineManager that discovers all script engine
        // factories (and their associated script engines) that are visible to
        // the current thread's classloader.

        ScriptEngineManager manager = new ScriptEngineManager();

        // Obtain a ScriptEngine that supports the JavaScript short name.
        ScriptEngine engine = manager.getEngineByName("JavaScript");

        // Specify a simple script to demonstrate compilation improvement.
        String script = "function sum (x)"
                + "{"
                + "   return x*(x+1)/2;"
                + "};";

        // Time script parsing and intermediate code execution.
        long now = System.currentTimeMillis();
        for (int i = 0; i < ITER_MAX; i++) {
            engine.eval(script);
        }
        System.out.println(System.currentTimeMillis() - now);

        Compilable compilable = null;
        if (engine instanceof Compilable) {
            compilable = (Compilable) engine;
            CompiledScript cs = compilable.compile(script);

          // Time intermediate code execution.
            now = System.currentTimeMillis();
            for (int i = 0; i < ITER_MAX; i++) {
                cs.eval();
            }
            System.out.println(System.currentTimeMillis() - now);
        }
    }
}

위 코드를 Intel i5-2500K CPU에서 Java 8의 Nashorn 엔진으로 실행해 보았더니 다음과 같은 결과가 나왔습니다. 컴파일을 미리 한 경우 비약적인 성능 향상이 있음을 알 수 있습니다.

639
23

참고로 동일한 코드를 Java 7으로 컴파일하여 Rhino 엔진으로 실행해 보았더니 다음과 같은 결과가 나왔습니다.

3262
1708

Rhino에 비해 Nashorn이 얼마나 압도적으로 성능이 우수한지 단편적으로 볼 수 있습니다.

참고로 Aryia Hidayat의 테스트에 의하면 Netscape에서 개발한 Rhino와 Oracle에서 개발한 Nashorn, 그리고 구글에서 개발한 V8 Javascript엔진의 성능은 다음과 같다고 합니다.



Nashorn은 Javascript 파일을 JVM을 위한 바이트코드로 변경하여 JVM의 최적화 JIT 엔진에 태우는 방식으로 획기적으로 성능을 개선했고, V8의 경우 아예 실시간으로 기계어로 변환하기 때문에 성능이 더 좋습니다. 성능은 V8이 더 나을지 몰라도 바이트코드로 변환하는 Nashorn 방식이 더 생산성이 높다고 생각됩니다. 어차피 JVM은 플랫폼 별로 포팅이 되니까요.

Invocable 인터페이스로 함수를 호출하기

Invocable 인터페이스는 Compilable의 사촌 정도 되는 인터페이스입니다. Invocable 인터페이스는 스크립트의 글로벌 함수와 객체의 멤버 함수를 실행할 수 있게 해 줍니다. 스크립트의 함수 호출 전에 eval() 메쏘드를 이용하여 미리 중간코드로 컴파일되어 있어야 하며, 따라서 스크립트 엔진의 상태에 저장된 중간 코드를 Invocable을 통해서 실행할 수 있습니다.

스크립트 엔진 중에서 이런 함수 호출을 지원하는 엔진은 Invocable 인터페이스를 구현하고 있습니다. 모든 엔진이 이 인터페이스를 구현하는 것이 아니기 때문에 다음과 같이 Invocable이 구현되었는지 먼저 테스트해 보아야 합니다.

ScriptEngine engine = ...;
Invocable invocable = null;
if (engine instanceof Invocable)
    invocable = (Invocable) engine;

스크립트 엔진을 Invocable 타입으로 캐스팅하고 난 다음에는 Invocable의 메쏘드인 invokeFunction(String name, Object... args)를 이용하여 글로벌 함수를 호춣ㄹ 수 있습니다. name 인자에는 함수의 이름을 넣고 args 인자에는 함수의 호출에 사용될 인자들을 넣습니다.

만일 주어진 함수 이름이 스크립트 엔진의 상태에 없는 것이라면 NoSuchMethodException 예외가 발생하고, 함수 호출에 그외 문제가 발생할 경우 ScriptException이 발생합니다. name에 null을 줄 경우는 NullPointerException이 발생합니다.

비슷한 방법으로 Invocable 인터페이스의 invokeMethod(Object thiz, String name, Object... args) 를 호출하면 객체의 멤버 함수를 호출할 수 있습니다. thiz 인자가 참조하고 있는 것은 멤버함수를 가지고 있는 객체입니다. 이 객체는 앞서 부른 스크립트 실행 혹은 함수의 호출로 얻을 수 있습니다. name 인자는 객체의 멤버함수 이름이고, args는 멤버함수에 넣을 인자들입니다.

invokeMethod는 invokeFunction()에서 발생하는 예외의 종류와 경로가 거의 비슷하지만, thiz에 주어진 객체가 null이거나 스크립트에서 얻어진 객체가 아닌 경우에는 IllegalArgumentException 예외가 발생됩니다.

다음 예제는 invokeFunction()과 invokeMethod() 메쏘드의 사용예를 보여 줍니다. makeEmployee() 함수는 객체를 만들게 되는데 멤버 함수로 print()를 정의합니다. 그리고 makeEmployee()함수의 결과로 만들어진 객체가 리턴됩니다.

// ScriptDemo7.java

import javax.script.*;

public class ScriptDemo7 {
    public static void main(String[] args) throws Exception {
      // Create a ScriptEngineManager that discovers all script engine
        // factories (and their associated script engines) that are visible to
        // the current thread's classloader.

        ScriptEngineManager manager = new ScriptEngineManager();

      // Obtain a ScriptEngine that supports the JavaScript short name.
        ScriptEngine engine = manager.getEngineByName("JavaScript");

      // Evaluate a script that defines a global function and an object.
        String script = "function makeEmployee(name, salary)"
                + "{"
                + "    var obj = new Object();"
                + "    obj.name = name;"
                + "    obj.salary = salary;"
                + "    obj.print = function()"
                + "    {"
                + "        print (\"name = \"+obj.name);"
                + "        print (\"salary = \"+obj.salary);"
                + "    };"
                + "    return obj;"
                + "}";

        engine.eval(script);

        Invocable invocable = null;
        if (engine instanceof Invocable) {
            invocable = (Invocable) engine;

          // Invoke the makeEmployee() function to make an object.
            Object emp = invocable.invokeFunction("makeEmployee", "John Doe", 40000.0);

          // Print the employee via the object's print() member function.
            invocable.invokeMethod(emp, "print");
        }
    }
}


이렇게 지금까지 4편의 포스팅을 통해 Java의 Scripting API의 개요와 사용법에 대해서 알아 보았습니다.

댓글 없음:

댓글 쓰기