Производительность PL/SQL - Intra-unit Inlining

ОГЛАВЛЕНИЕ

Intra-unit Inlining

Intra-unit inlining представляет собой подмену вызова подпрограммы на копию кода этой подпрограммы. В результате модифицированный код выполняется быстрее. В Oracle Database 11g компилятор PL/SQL способен идентифицировать вызовы подпрограммы, которую необходимо скопировать (другими словами, подменить на неё) и делает изменения, улучшающие производительность.

Лучше всего это объяснить на примере. Код, показанный ниже, изменяет таблицу BALANCES, вычислив значения на основании баланса счёта. Код проходит по всем записям таблицы, вычисляет результат, и изменяет столбец таблицы с балансом.

create or replace procedure upd_int is
/* original version */
    l_rate_type     balances.rate_type%type;
    l_bal           balances.balance%type;
    l_accno         balances.accno%type;
    l_int_rate      number;
    procedure calc_int (
        p_bal in out balances.balance%type,
        p_rate  in number
    ) is
    begin
        if (p_rate >= 0) then
            p_bal := p_bal * (1+(p_rate/12/100));
        end if;
    end;
begin
    for ctr in 1..10000 loop
        l_accno := ctr;
        select balance, rate_type
        into l_bal, l_rate_type
        from balances
        where accno = l_accno;
        select decode(l_rate_type,
            'C', 1, 'S', 3, 'M', 5, 0)
        into l_int_rate
        from dual;
        for mth in 1..12 loop
            calc_int (l_bal, l_int_rate);
            update balances
            set balance = l_bal
            where accno = l_accno;
        end loop;
    end loop;
end;
/
Фактически, вычисление результата одинаково для всех типов записей, и я поместил логику в отдельную процедуру calc_int() внутри основной процедуры. Это повышает читаемость и сопровождаемость кода, но, к сожалению, это неэффективно.

Однако, если заменить вызов calc_int() на код calc_int(), получится более быстрая программа, как показано ниже:

create or replace procedure upd_int is
/* revised version */
    l_rate_type     balances.rate_type%type;
    l_bal           balances.balance%type;
    l_accno         balances.accno%type;
    l_int_rate      number;
begin
    for ctr in 1..10000 loop
        l_accno := ctr;
        select balance, rate_type
        into l_bal, l_rate_type
        from balances
        where accno = l_accno;
        select decode(l_rate_type,
            'C', 1, 'S', 3, 'M', 5, 0)
        into l_int_rate
        from dual;
        for mth in 1..12 loop
            -- this is the int calc routine
            if (l_int_rate >= 0) then
                l_bal := l_bal * (1+(l_int_rate/12/100));
            end if;
            update balances
            set balance = l_bal
            where accno = l_accno;
        end loop;
    end loop;
end;
/
Этот переделанный код отличается от исходного только в части кода для вычисления баланса, который теперь внутри цикла, а не в процедуре calc_int().

Заметьте, что новая версия может быть быстрее, но это не очень хороший пример практики кодирования. Часть кода, выполняющая вычисление баланса, выполняется один раз для каждой итерации цикла для месяцев, а затем и для каждого номера счёта. Так как эта часть кода повторяется, она более удобна для размещения отдельно, как показано в предыдущей версии upd_int, в процедуре (calc_int). Этот подход делает код модульным, легким в поддержке, и реально читаемым — но также менее эффективным.

Поэтому как можно достичь примирения конфликтующих способов создания кода, сделав код модульным и одновременно быстрым? Так, а можно ли написать код, используя модульный подход (как в первой версии upd_int), а затем позволить компилятору PL/SQL "оптимизировать" его, чтобы он стал выглядеть, как во второй версии кода?

Это можно сделать в Oracle Database 11g. Всё, что требуется сделать - перекомпилировать процедуру с более высоким уровнем оптимизации PL/SQL. Этого можно достичь двумя способами:

  • Установить параметр уровня сессии и перекомпилировать процедуру:
    SQL> alter session set plsql_optimize_level = 3; 
    Session altered.
    Команда, показанная выше, инструктирует компилятор PL/SQL, чтобы он переписал код во встроенный код.
  • Скомпилировать процедуру непосредственно с plsql-установкой.
    SQL> alter procedure upd_int
      2  compile
      3  plsql_optimize_level = 3
      4  reuse settings;
     
    Procedure altered.
    На любую другую процедуру, компилируемую в этой же сессии, это не повлияет. Этот метод лучше применять для обработки inlining, если есть много процедур, которые необходимо скомпилировать в одной сессии.
create or replace procedure upd_int is
    l_rate_type     varchar2(1);
...
...
begin
    pragma inline (calc_int, 'YES');
    for ctr in 1..10000 loop
...
...
end;
Я добавил строку pragma inline (calc_int, 'YES'); для указания компилятору подменить в коде эту процедуру. Таким же образом можно указать "NO" в этом же месте, чтобы передать компилятору, что не надо подменять эту процедуру, даже если plsql_optimizer_level установлен в значение 3.

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