Tanto en la documentación de Oracle Corporation como en su página de metalink hay aseveraciones que nos dicen que el PL/SQL es mucho más rápido si se trabaja en forma compilada -compilación nativa- que si se interpretara. Parece real si pensamos en el tiempo de respuesta de un lenguaje interpretado contra código que ha sido compilado y optimizado. Oracle Corporation habla en presentaciones de mercadeo sobre los beneficios de la compilación nativa en Oracle 11g, decidí hacer un experimento para validar la declaración sobre la compilación nativa de PL/SQL.
Las pruebas fueron hechas tanto sobre Oracle 10.2.0.3, sobre Oracle 11.1.0.6 y sobre 9.2.0.8. Las tres versiones sobre Linux. Los resultados fueron similares, aquí documento únicamente el caso sobre Oracle 11.1.0.6 y 10.2.0.3.
El procedimiento almacenado native1 y native2 son idénticos salvo que native2 está compilado en forma nativa.
create or replace procedure native1 as
i number;
j number;
k number;
str varchar2(10);
begin
for i in 1 .. 500000 loop
k:=dbms_random.value;
j:=i*k;
str:=rpad('X', k*10, 'X');
end loop;
end;
/
Oracle 11.1.0.6
SQL> exec native1;
PL/SQL procedure successfully completed.
Elapsed: 00:02:01.26
SQL> exec native2
PL/SQL procedure successfully completed.
Elapsed: 00:02:06.88
Oracle 10.2.0.3
SQL> exec native1;
PL/SQL procedure successfully completed.
Elapsed: 00:02:06.88
SQL> exec native2;
PL/SQL procedure successfully completed.
Elapsed: 00:02:13.48
Hasta ahora tenemos que al menos para la función dbms_random es más eficiente utilizar código interpretado en PL/SQL. ¿Qué tal si el código compilado en forma nativa es más eficiente cuando se manipula una cantidad mayor de datos?
create or replace procedure native3 as
i number;
j number;
k number;
str varchar2(10);
begin
for i in 1 .. 50000000 loop
k:=i+1;
j:=i*k;
str:=rpad('X', 5, 'X');
end loop;
end;
/
native3 Elapsed: 00:00:20.18 –compilación nativa
native4 Elapsed: 00:00:16.69 –compilación interpretada
El patrón es similar, es más rápida la compilación interpretada que la compilación nativa. ¡Qué sorpresa dada la declaración de Oracle Corporation!
Después hice pruebas utilizando distintas funciones -por si el problema era únicamente con la función dbms_random- y el resultado en cuanto a tiempo de ejecución del código que utiliza NCOMP (native compilation) contra el tiempo de ejecución del código interpretado fue consistente, NCOMP es más lento.
Está bien demostrar que para los casos probados la declaración resultó falsa. Pero hay casos en la documentación donde claramente los tiempos de NCOMP son menores que los tiempos de ejecución del código interpretado. La declaración de Oracle Corporation en cuanto a NCOMP dice que NCOMP es más rápido que el código interpretado para los casos en que no se accede a tablas. Dado que la declaración resultó ser falsa, ¿cuál es la razón de que en algunos casos NCOMP sea más rápido que el código interpretado?
A pesar de que Oracle es un producto muy maduro, aún hay pequeñísimas excepciones en cuanto a la medición de los tiempos por evento. Así que tuve que recurrir al sistema operativo para detectar dónde NCOMP dedica el tiempo que aparece como tiempo de resolución de sentencias en los traces del RDBMS.
El strace reportó el consumo del tiempo a nivel sistema operativo
Compilación interpretada
30341 0.000000 [ffffe410] read(9, "\x00\xe0\x00\x00\x06\x00\x00\x00\x00\x00\x11\x69\x3 b\xfe\xff\xff\xff\x02\x00\x00\x00\x08\x00\x00\x00\x09\x00\x00\x00\x03\x5e\x3c"..., 8208 ) = 224 <5.599394>
30341 5.599585 [ffffe410] gettimeofday({1206729344, 794468}, NULL) = 0 <0.000011>
30341 0.000083 [ffffe410] getrusage(RUSAGE_SELF, {ru_utime={892, 255762}, ru_stime={4, 132258}, ...}) = 0 <0.000011>
30341 0.000098 [ffffe410] times(NULL) = 1724918357 <0.000010>
donde el FD del proceso 30341 es un pipe
oracle 30341 root 9r FIFO 0,6 714714 pipe
NCOMP
12320 0.000000 [ffffe410] read(9, "\x00\xdc\x00\x00\x06\x00\x00\x00\x00\x00\x11\x69\xab\xfe\xff\xff\xff\x01\x00\x00\x00\x08\x00\x00\x00\x03\x5e\xac\x21\x00\x04\x00"..., 8208 ) = 220 <1.550380>
12320 4.715362 [ffffe410] gettimeofday({1206729102, 897396}, NULL) = 0 <0.000007>
12320 0.000056 [ffffe410] getrusage(RUSAGE_SELF, {ru_utime={1715, 927238}, ru_stime={4, 276267}, ...}) = 0 <0.000006>
12320 0.000053 [ffffe410] times(NULL) = 1724894166 <0.000005>
donde el FD del read es un datafile de system
oracle 12320 root 8u REG 8,9 367017984 146388 /u030/data/oracle/PROD111/system01.dbf
el detalle de strace es el siguiente
Compilación interpretada
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
78.26 0.000054 0 1990 getrusage
21.74 0.000015 0 1698 gettimeofday
0.00 0.000000 0 2 read
0.00 0.000000 0 2 write
0.00 0.000000 0 174 times
0.00 0.000000 0 31 pread64
0.00 0.000000 0 1 semctl
------ ----------- ----------- --------- --------- ----------------
100.00 0.000069 3898 total
NCOMP
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
nan 0.000000 0 1 read
nan 0.000000 0 1 write
nan 0.000000 0 31 times
nan 0.000000 0 357 getrusage
nan 0.000000 0 468 gettimeofday
------ ----------- ----------- --------- --------- ----------------
100.00 0.000000 858 total
Así que las funciones de PL/SQL no se resuelven en memoria como lo haría una función típica que utiliza las librerías del sistema operativo. Aparentemente las funciones de PL/SQL acceden al datafile de system, de hecho como pudo verse, la función sobre la que se hizo el trace accedió al mismo bloque del datafile de system.
Así que la prueba sugiere que NCOMP es más rápido si no se accede en ningún sentido a un bloque de un datafile, mientras que el código interpretado es más rápido si se acceden bloques de un datafile.
La prueba no debe de extrapolarse a como regla genérica, recomiendo probar cada caso porque hay situaciones que pueden tener un impacto importante. ¿Qué tal si se accede a un bloque y el resto del PL/SQL -que pudiera ser enorme- hace muchísimas actividades que efectivamente se llevarán a cabo en memoria? Aún en el caso anterior valdrá la pena probar utilizando funciones, pero por otro lado el llamar funciones tiene un costo intrínseco porque necesitan generarse context switches.