Quel est le coût de la JVM dans Oracle 11g ?

Oracle 11g a introduit le JIT compiler pour JServer. Celui-ci remplace la compilation statique avec ncomp de la 10g et transforme les classes Java chargées dans la base Oracle en code natif de manière transparente, selective et optimale… Oublions les aspects « selectif et optimal » pour l’instant et faisons confiance à Oracle pour cela (quoique !). Pour l’aspect transparent, l’exemple ci-dessous le démontre assez bien.

Cela étant, c’est un peu loin de ma préoccupation de départ qui consistait à évaluer le coût d’un appel Java dans la base Oracle et de le comparer à la fois à un appel Java depuis une JVM classique ou un appel PL/SQL. Mais j’y viens…

Une classe Java

Pour commencer, voici le code source d’une classe Java sans ambition et qui ne fait surtout rien nommée HelloWorld.java :

public class HelloWorld {
public static void main(String[] args) {
getOutput("X");
}

public static String getOutput(String val) {
return val + " !!!";
}
}

Pour fixer les idées, voici ce qui se passe quand on lance 1000x fois une JVM :

javac HelloWorld.java 
time for i in {1..1000}; do X=1; done
real 0m0.011s
user 0m0.012s
sys 0m0.000s

time for i in {1..1000}; do java HelloWorld; done
real 1m42.970s
user 0m42.971s
sys 0m14.265s

Evidemment, ce qu’on mesure ici c’est le temps d’instantiation de 1000 JVM et, heureusement, Oracle n’instantie pas une JVM à chaque appel Java.

Une vrai temps de référence

Pour avoir un temps de référence optimiste, j’ai développé un programme en C qui n’instantie la JVM qu’une seule fois et appelle un million de fois notre classe. Pour cela, j’utilise l’API Invocation inclus dans JNI. Voici un programme callJava.c ci-dessous qui réutilise le code fournit par Sun :

/*
* Copyright (c) 1995-1997 Sun Microsystems, Inc. All Rights Reserved.
*
* Permission to use, copy, modify, and distribute this software
* and its documentation for NON-COMMERCIAL purposes and without
* fee is hereby granted provided that this copyright notice
* appears in all copies. Please refer to the file "copyright.html"
* for further important copyright and licensing information.
*
* SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF
* THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
* TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR
* ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
* DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
*/

#include <stdlib.h>
#include <stdio.h>
#include <jni.h>

/* This is the program's "main" routine. */
int main (int argc, char *argv[]) {

JavaVM *jvm; /* denotes a Java VM */
JNIEnv *env; /* pointer to native method interface */
JavaVMInitArgs vm_args;
JavaVMOption options[1];

jint res;
jclass cls;
jmethodID mid;

/* IMPORTANT: need to specify vm_args version especially if you are not using JDK1.1.
* Otherwise, will the compiler will revert to using the 'JDK1_1InitArgs' struct.
*/
vm_args.version = JNI_VERSION_1_4;

/* This option doesn't do anything, just to illustrate how to pass args to JVM. */
options[0].optionString = "-verbose:none";
vm_args.nOptions = 1;
vm_args.options = options;
vm_args.ignoreUnrecognized = JNI_FALSE;

/* load and initialize a Java VM, return a JNI interface pointer in env */
res = JNI_CreateJavaVM(&jvm,(void**)&env,&vm_args);
if (res < 0) {
fprintf(stderr, "Can't create Java VMn");
exit(1);
}

jclass ver;
jmethodID print;

ver = (*env)->FindClass(env, "sun/misc/Version");
if (ver == 0) {
fprintf(stderr, "Can't find Version");
}
print = (*env)->GetStaticMethodID(env, ver, "print", "()V");
(*env)->CallStaticVoidMethod(env, ver, print);

long i;
for (i=0;i<1000000;i++) {

/* invoke the Main.test method using the JNI */
cls = (*env)->FindClass(env, "HelloWorld");
if (cls == 0) {
fprintf(stderr, "Can't find HelloWorld.classn");
exit(1);
}

mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;)V");
if (mid==0) {
fprintf(stderr, "No such method!n");
exit(1);
}

// otherwise execute this method
(*env)->CallStaticVoidMethod(env, cls, mid);
}

/* We are done. */
(*jvm)->DestroyJavaVM(jvm);

return 0;
}

A l’exécution, les statistiques ont un résultat tout autre :

export JAVA_HOME=/usr/lib/jvm/java-6-sun-1.6.0.24
export PATH=$JAVA_HOME/bin:$PATH
export LD_LIBRARY_PATH=$JAVA_HOME/jre/lib/i386/client
gcc -g -Wall -I$JAVA_HOME/include
-I$JAVA_HOME/include/linux
-L$JAVA_HOME/jre/lib/i386/client/
-ljvm -o callJava callJava.c

$JAVA_HOME/bin/javac HelloWorld.java

$ time ./callJava
java version "1.6.0_24"
Java(TM) SE Runtime Environment (build 1.6.0_24-b07)
Java HotSpot(TM) Client VM (build 19.1-b02, mixed mode, sharing)

real 0m1.245s
user 0m1.228s
sys 0m0.008s

Evidemment les méthodes Java ne font rien que d’affecter des valeurs et HotSpot utilise lui-même une approche de compilation JIT. Toutefois, c’est assez fulgurant, non ?

Notes:
Je rencontre un bug assez proche du bug 5099186 avec Java SE 5.0 qui devrait servir de référence puisque JServer utilise la version 1.5.
D’après l’expérience de « Vincent », il est possible que Java Native Access soit encore plus rapide, mais c’est déjà une autre histoire

Evidemment, étant donné l’architecture de JServer décrite dans le livre de Kuassi, on ne peut pas s’attendre à ce genre de performance mais disons que ça fixe la barre.

JServer sans JIT

On va charger la classe dans Oracle et l’exécuter depuis un wrapper PL/SQL pour fixer les idées; pour cela désactivons la compilation JIT :

sqlplus / as sysdba
SQL> alter system set java_jit_enabled=false;
SQL> startup force;
SQL> exit;

loadjava -u SCOTT -resolve HelloWorld.java

Password:
******

sqlplus scott/tiger

COL object_name format a11
COL object_type format a15
SELECT object_name, object_type, status
FROM user_objects
WHERE object_type IN
('JAVA SOURCE', 'JAVA CLASS', 'JAVA RESOURCE')
ORDER BY object_type, object_name;

OBJECT_NAME OBJECT_TYPE STATUS
----------- --------------- -------
HelloWorld JAVA CLASS VALID
HelloWorld JAVA SOURCE VALID


col name format a11
col method_name format a11
col is_compiled format a10
select name, METHOD_NAME, IS_COMPILED
from user_java_methods;

NAME METHOD_NAME IS_COMPILE
----------- ----------- ----------
HelloWorld NO
HelloWorld main NO
HelloWorld getOutput NO



CREATE OR REPLACE FUNCTION HelloWorld (sval varchar2) RETURN varchar2
AS LANGUAGE JA VA
NAME 'HelloWorld.getOutput(java.lang.String) return java.lang.String';
/

var x varchar2(100)
exec :x := HelloWorld ('ArKZoYd')

print x

X
-------------
ArKZoYd !!!

set timing on
declare
x varchar2(100);
begin
for i in 1..1000000 loop
x:= HelloWorld (to_char(i));
end loop;
end;
/

PL/SQL procedure successfully completed.

Elapsed: 00:00:33.31


col name format a11
col method_name format a11
col is_compiled format a10
select name, METHOD_NAME, IS_COMPILED
from user_java_methods;

NAME METHOD_NAME IS_COMPILE
----------- ----------- ----------
HelloWorld NO
HelloWorld main NO
HelloWorld getOutput NO


SQL> exit;

Evidemment, ça parait bien moins rapide que depuis un process écrit en C; cela dit, Oracle offre une architecture capable de monter en charge et facilement accessible depuis du PL/SQL.

JServer avec JIT

On va charger la classe dans Oracle et l’exécuter depuis un wrapper PL/SQL pour fixer les idées; avant ça, ré-activons la compilation JIT :

sqlplus / as sysdba
SQL> alter system set java_jit_enabled=false;
SQL> startup force;
SQL> exit;

dropjava -u SCOTT HelloWorld.java

Password:
******

loadjava -u SCOTT -resolve HelloWorld.java

Password:
******

sqlplus scott/tiger

COL object_name format a11
COL object_type format a15
SELECT object_name, object_type, status
FROM user_objects
WHERE object_type IN
('JAVA SOURCE', 'JAVA CLASS', 'JAVA RESOURCE')
ORDER BY object_type, object_name;

OBJECT_NAME OBJECT_TYPE STATUS
----------- --------------- -------
HelloWorld JAVA CLASS VALID
HelloWorld JAVA SOURCE VALID


col name format a11
col method_name format a11
col is_compiled format a10
select name, METHOD_NAME, IS_COMPILED
from user_java_methods;

NAME METHOD_NAME IS_COMPILE
----------- ----------- ----------
HelloWorld NO
HelloWorld main NO
HelloWorld getOutput NO



CREATE OR REPLACE FUNCTION HelloWorld (sval varchar2) RETURN varchar2
AS LANGUAGE JAVA
NAME 'HelloWorld.getOutput(java.lang.String) return java.lang.String';
/

var x varchar2(100)
exec :x := HelloWorld ('ArKZoYd')

print x

X
-------------
ArKZoYd !!!

set timing on
declare
x varchar2(100);
begin
for i in 1..1000000 loop
x:= HelloWorld (to_char(i));
end loop;
end;
/

PL/SQL procedure successfully completed.

Elapsed: 00:00:25.30

set timing on
declare
x varchar2(100);
begin
for i in 1..1000000 loop
x:= HelloWorld (to_char(i));
end loop;
end;
/

PL/SQL procedure successfully completed.

Elapsed: 00:00:25.26


col name format a11
col method_name format a11
col is_compiled format a10
select name, METHOD_NAME, IS_COMPILED
from user_java_methods;

NAME METHOD_NAME IS_COMPILE
----------- ----------- ----------
HelloWorld NO
HelloWorld main NO
HelloWorld getOutput YES


SQL> exit;

dropjava -u SCOTT HelloWorld.java

Password:
******

Le 2nd test ci-dessous met en évidence que la compilation JIT fonctionne correctement. Dans ce cas on constate même une légère amélioration d’environ 20%. Evidemment, ça ne sera pas toujours le cas. Si votre serveur a plusieurs processeurs, un test en parallèle montre également une vrai capacité de monter en charge.

Conclusion

Avec plus de 30000 appels par secondes et par processeur dans le cas de mon PC, ça donne une idée de ce qu’on peut faire… Evidemment, il ne s’agit pas du tout de comparer Java et PL/SQL mais simplement le temps d’instantiation d’une procédure Java comparée à une procédure PL/SQL. Ce dernier, quand il ne fait rien, reste imbattable :

create or replace function HelloWorld (sval varchar2) RETURN varchar2 is
begin
return sval||' !!!';
end;
/

set timing on
declare
x varchar2(100);
begin
for i in 1..1000000 loop
x:= HelloWorld (to_char(i));
end loop;
end;
/

PL/SQL procedure successfully completed.

Elapsed: 00:00:00.67


drop function HelloWorld;