Производительность PL/SQL - Выполнение эксперимента

ОГЛАВЛЕНИЕ

Выполнение эксперимента

Теперь настало время проверить эти достоинства, повторяя эксперимент. Создадим базовую версию пакета, а затем модифицируем тип данных переменных и директивы компилятора в соответствии с концепциями, изученными перед этим.

Сначала реализуем Euclidean algorithm (алгоритм Евклида) для поиска наибольшего общего делителя для двух чисел. Вот логика функции, со страницы Wiki:

function gcd(a, b)
     if a = 0 return b
     while b ? 0
         if a > b
             a := a - b
         else
             b := b - a
     return a
Замерим процессорное время при выполнении пакета при различных комбинациях этих модификаций и сохраним затраченное время. Итак, создаём таблицу для сохранения процессорного времени:
create table times(
  native        char(1) check (native    in ('Y', 'N')) enable,
  simple        char(1) check (simple    in ('Y', 'N')) enable,
  inlining      char(1) check (inlining  in ('Y', 'N')) enable,
  centiseconds  number not null,
  constraint times_pk primary key (simple, inlining, native))
/
Сделаем три столбца — native, simple и inlining — для обозначения Native compilation, чисел типа simple_integer, и inlined-кода, соответственно. "Y" в столбце означает, что код скомпилирован с использованием этой возможности. Итак, получается запись, которая выглядит так:
NATIVE SIMPLE INLINING CENTISECONDS
------ ------ -------- ------------
Y      N      N                 100
Она показывает, что программа была скомпилирована в режиме Native compilation, но числа типа simple_integer не использовались, inlining не применялось, и что эта компиляция заняла 100 сёнтисекунд процессорного времени.

Чтобы использовать только одну копию пакета, для модификации пакета применим условную компиляцию (появилась в Oracle Database 10g Release 2). Ниже показано, как создать пакет:

-- подавляем следующие ожидаемые предупреждения:
--   inlining of call of procedure 'gcd' was done
--   uncalled procedure "gcd" is removed.
--   unreachable code
--   keyword "native" used as a defined name
alter session set plsql_warnings = 'enable:all, disable:06002, disable:06005, disable:06006, disable:06010'
/
alter session set plsql_ccflags = 'simple:false'
/
create package gcd_test is
  procedure time_it;
end gcd_test;
/
create package body gcd_test is
  $if $$simple $then
    subtype my_integer is simple_integer;
    simple constant times.simple%type := 'y';
  $else
    subtype my_integer is pls_integer not null;
    simple constant times.simple%type := 'n';
  $end

  function gcd(p1 in my_integer, p2 in my_integer) return my_integer is
    v1 my_integer := p1; v2 my_integer := p2;
  begin
    while v2 > 0 loop
      if v1 > v2 then
        v1 := v1 - v2;
      else
        v2 := v2 - v1;
      end if;
    end loop;
    return v1;
  end gcd;

  function exercise_gcd return number is
   -- ожидается значение, зависящее от no_of_iterations.
    expected_checksum my_integer := 74069926; -- 2475190;
    no_of_iterations constant my_integer := 5000; -- 1000;
    checksum my_integer := 0;
    v my_integer := 0;
    t0 number; t1 number;
  begin
    for warmup in 1..2 loop
      checksum := 0;
      t0 := dbms_utility.get_cpu_time();

      for j in 1..no_of_iterations loop
        v := gcd(j, j);
        if v <> j then
          raise_application_error(-20000, 'logic error: gcd(j, j) <> j');
        end if;
        checksum := checksum + v;

        for k in (j + 1)..no_of_iterations loop
          v := gcd(j, k);
          if gcd(k, j) <> v then
            raise_application_error(-20000, 'logic error: gcd(j, k) <> gcd(k, j)');
          end if;
          checksum := checksum + v;
        end loop;
      end loop;

      if checksum <> expected_checksum then
        raise_application_error(-20000, 'checksum <> expected_checksum: '||checksum);
      end if;

      t1 := dbms_utility.get_cpu_time();
    end loop;
   return t1 - t0;
  end exercise_gcd;

  procedure time_it is
    inlining times.inlining%type;
    native times.native%type;
    centiseconds constant times.centiseconds%type := exercise_gcd();
  begin
    if lower($$plsql_code_type) = 'native' then
      native := 'y';
    else
      native := 'n';
    end if;

    if $$plsql_optimize_level = 3 then
      inlining := 'y';
    else
      inlining := 'n';
    end if;

    insert into times(native, simple, inlining, centiseconds)
      values(time_it.native, gcd_test.simple, time_it.inlining, time_it.centiseconds);
    commit;
  end time_it;
end gcd_test;
/
show errors
Пакет имеет достаточно однострочных комментариев для самодокументации кода, поэтому здесь подробные объяснения не требуются. А в общем, функция GCD() имеет два входных параметра и возвращает наибольший общий делитель. Функция exercise_gcd() вызывает функцию GCD() и возвращает время работы процессора (CPU time) в сёнтисекундах. И public-процедураTIME_IT() вызывает функцию EXERCISE_GCD() для соответствующей степени свободы и вставляет запись в таблицу TIMES.

Теперь надо несколько раз вызвать пакетную процедуру с различными параметрами и зафиксировать время работы процессора в каждом случае. Выполним это, изменив переменные условной компиляции перед тем, как компилировать пакет:

truncate table times
/

-- Interpreted ---------------------------------------------
-- Simple:false
-- No Inlining
alter package GCD_Test compile body
  PLSQL_Code_Type = interpreted
  PLSQL_CCFlags = 'Simple:false'
  PLSQL_Optimize_Level = 2 /* no inlining */
  reuse settings
/
begin GCD_Test.Time_It(); end;
/

-- inlining
alter package GCD_Test compile body
  PLSQL_Code_Type = interpreted
  PLSQL_CCFlags = 'Simple:false'
  PLSQL_Optimize_Level = 3 /* inlined */
  reuse settings
/
begin GCD_Test.Time_It(); end;
/

-- Числа типа simple_integer: используются
-- Без inlining

alter package GCD_Test compile body
  PLSQL_Code_Type = interpreted
  PLSQL_CCFlags = 'Simple:true'
  PLSQL_Optimize_Level = 2
  reuse settings
/
begin GCD_Test.Time_It(); end;
/

-- inlined
alter package GCD_Test compile body
  PLSQL_Code_Type = interpreted
  PLSQL_CCFlags = 'Simple:true'
  PLSQL_Optimize_Level = 3
  reuse settings
/
begin GCD_Test.Time_It(); end;
/

-- Native -------------------------------------------------
-- Числа типа simple_integer: не используются
-- Без inlining
alter package GCD_Test compile body
  PLSQL_Code_Type = native
  PLSQL_CCFlags = 'Simple:false'
  PLSQL_Optimize_Level = 2
  reuse settings
/
begin GCD_Test.Time_It(); end;
/

-- inlined
alter package GCD_Test compile body
  PLSQL_Code_Type = native
  PLSQL_CCFlags = 'Simple:false'
  PLSQL_Optimize_Level = 3
  reuse settings
/
begin GCD_Test.Time_It(); end;
/

-- Числа типа simple_integer: используются
-- Без linlining

alter package GCD_Test compile body
  PLSQL_Code_Type = native
  PLSQL_CCFlags = 'Simple:true'
  PLSQL_Optimize_Level = 2
  reuse settings
/
begin GCD_Test.Time_It(); end;
/

-- inlined
alter package GCD_Test compile body
  PLSQL_Code_Type = native
  PLSQL_CCFlags = 'Simple:true'
  PLSQL_Optimize_Level = 3
  reuse settings
/
begin GCD_Test.Time_It(); end;
/
Для определения, насколько повышается производительность при каждом из этих сценариев, используем следующий код:
spool timings.txt
<<b>>declare
  Interp_Pls_Integer_Noinline     Times.Centiseconds%type;
  Interp_Pls_Integer_Inline       Times.Centiseconds%type;
  Interp_Simple_Integer_Noinline  Times.Centiseconds%type;
  Interp_Simple_Integer_Inline    Times.Centiseconds%type;

  Native_Pls_Integer_Noinline     Times.Centiseconds%type;
  Native_Pls_Integer_Inline       Times.Centiseconds%type;
  Native_Simple_Integer_Noinline  Times.Centiseconds%type;
  Native_Simple_Integer_Inline    Times.Centiseconds%type;

  procedure Show_Caption(Caption in varchar2) is
  begin
    DBMS_Output.Put_Line(Chr(10)||Rpad('-', 60, '-')||Chr(10)||Chr(10)||Caption||Chr(10));
  end Show_Caption;

  procedure Show_Ratio(Var1 in varchar2, Var2 in varchar2, Ratio in number) is
  begin
    DBMS_Output.Put_Line(Rpad(Var1, 15)||'and '||Rpad(Var2, 14)||To_Char(Ratio, '99.99'));
  end Show_Ratio;
begin
  select  a.Centiseconds
  into    b.Interp_Pls_Integer_Noinline
  from    Times a
  where   a.Native = 'N' and a.Simple = 'N' and a.Inlining = 'N';

  select  a.Centiseconds
  into    b.Interp_Pls_Integer_Inline
  from    Times a
  where   a.Native = 'N' and a.Simple = 'N' and a.Inlining = 'Y';

  select  a.Centiseconds
  into    b.Interp_Simple_Integer_Noinline
  from    Times a
  where   a.Native = 'N' and a.Simple = 'Y' and a.Inlining = 'N';

  select  a.Centiseconds
  into    b.Interp_Simple_Integer_Inline
  from    Times a
  where   a.Native = 'N' and a.Simple = 'Y' and a.Inlining = 'Y';

  select  a.Centiseconds
  into    b.Native_Pls_Integer_Noinline
  from    Times a
  where   a.Native = 'Y' and a.Simple = 'N' and a.Inlining = 'N';

  select  a.Centiseconds
  into    b.Native_Pls_Integer_Inline
  from    Times a
  where   a.Native = 'Y' and a.Simple = 'N' and a.Inlining = 'Y';

  select  a.Centiseconds
  into    b.Native_Simple_Integer_Noinline
  from    Times a
  where   a.Native = 'Y' and a.Simple = 'Y' and a.Inlining = 'N';

  select  a.Centiseconds
  into    b.Native_Simple_Integer_Inline
  from    Times a
  where   a.Native = 'Y' and a.Simple = 'Y' and a.Inlining = 'Y';

  Show_Caption('Benefit of simple_integer');
  Show_Ratio('Interpreted',    'no inlining',    Interp_Pls_Integer_Noinline   / Interp_Simple_Integer_Noinline);
  Show_Ratio('Interpreted',    'inlining',       Interp_Pls_Integer_Inline     / Interp_Simple_Integer_Inline);
  Show_Ratio('Native',         'no inlining',    Native_Pls_Integer_Noinline   / Native_Simple_Integer_Noinline);
  Show_Ratio('Native',         'inlining',       Native_Pls_Integer_Inline     / Native_Simple_Integer_Inline);

  Show_Caption('Benefit of inlining');
  Show_Ratio('Interpreted',    'pls_integer',    Interp_Pls_Integer_Noinline    / Interp_Pls_Integer_Inline);
  Show_Ratio('Interpreted',    'simple_integer', Interp_Simple_Integer_Noinline / Interp_Simple_Integer_Inline);
  Show_Ratio('Native',         'pls_integer',    Native_Pls_Integer_Noinline    / Native_Pls_Integer_Inline);
  Show_Ratio('Native',         'simple_integer', Native_Simple_Integer_NoInline / Native_Simple_Integer_Inline);

  Show_Caption('Benefit of native');
  Show_Ratio('pls_integer',    'no inlining',    Interp_Pls_Integer_Noinline    / Native_Pls_Integer_Noinline);
  Show_Ratio('pls_integer',    'inlining',       Interp_Pls_Integer_Inline      / Native_Pls_Integer_Inline);
  Show_Ratio('simple_integer', 'no inlining',    Interp_Simple_Integer_Noinline / Native_Simple_Integer_Noinline);
  Show_Ratio('simple_integer', 'inlining',       Interp_Simple_Integer_Inline   / Native_Simple_Integer_Inline);

end b;
/
spool off
Ниже показан результат. Он показывает рейтинг процессорного времени по сравнению со значением по умолчанию: без inlining, interpreted compilation, и использование pls_integer.
------------------------------------------------------------

Преимущества simple_integer

Интерпретирующая    и без inlining      1.00
Интерпретирующая    и c inlining        1.00
Native              и без inlining      2.19
Native              и с inlining        2.79

------------------------------------------------------------

Преимущества  inlining

Интерпретирующая    и pls_integer     1.07
Интерпретирующая    и simple_integer  1.07
Native              и pls_integer     1.16
Native              и simple_integer  1.48

------------------------------------------------------------

Преимущества Native compilation

pls_integer       и без inlining      4.78
pls_integer       и inlining          5.18
simple_integer    и без inlining     10.53
simple_integer    и inlining         14.49
Из показанных выше результатов видно, что процессорное время, потраченное на выполнение, было 14.49 для значения по умолчанию по сравнению с Native compilation с inlining и simple_integer — очень приятный результат, по любым стандартам.