Оптимизирующий компилятор PL/SQL: принципы работы

Оптимизирующий компилятор языка PL/SQLОптимизирующий компилятор PL/SQL способен заметно ускорить выполнение кода за счет относительно невысоких затрат на стадии компиляции. Преимущества оптимизации распространяются как на интерпретируемый, так и компилируемый код PL/SQL, поскольку оптимизации применяются на основании анализа закономерностей в исходном коде.

Оптимизирующий компилятор активен по умолчанию. Тем не менее иногда приходится изменять его поведение — снижать уровень оптимизации или даже полностью отключать ее. Например, если в ходе выполнения обычных операций системе приходится перекомпилировать большой объем кода или если приложение генерирует много строк динамически выполняемого кода PL/SQL, затраты ресурсов на оптимизацию могут оказаться неприемлемыми. Однако следует учитывать, что по тестам Oracle оптимизация в среднем вдвое повышает производительность кода PL/SQL, сопряженного с большим объемом вычислений.


Оглавление статьи[Показать]


В некоторых случаях оптимизация может даже изменить поведение программы. Примеры такого рода встречаются в коде, написанном для Oracle9i, зависящим от относительного порядка выполнения инициализации в нескольких пакетах. Если в ходе тестирования проявляются подобные проблемы, но при этом вы хотите пользоваться преимуществами оптимизации, вам придется переписать часть кода или определить блок инициализации, обеспечивающий нужный порядок выполнения.

Уровень оптимизации задается параметром PLSQL_0PTIMIZE_LEVEL (и соответствующими командами DDL ALTER), которому может быть присвоено значение 0, 1, 2 или 3 (последнее доступно только в Oracle11g). Чем выше уровень, тем больше усилий приложит компилятор.

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

  •  PLSQL_0PTIMIZE_LEVEL = 0 — оптимизация отключена. Компилятор PL/SQL сохраняет исходный порядок обработки команд, используемый в Oracle9i и более ранних версиях. Код будет работать быстрее, чем в старых версиях, но различия не столь значительны.
  •  PLSQL_0PTIMIZE_LEVEL = 1 — компилятор применяет многочисленные оптимизации (такие, как удаление неиспользуемых вычислений и исключений). В общем случае порядок выполнения исходного кода остается неизменным.
  •  PLSQL_0PTIMIZE_LEVEL = 2 — значение по умолчанию, самый высокий уровень оптимизации до Oracle11g. Применяет множество современных методов оптимизации, выходящих за рамки уровня 1, причем некоторые изменения могут привести к перемещению кода на достаточно большое расстояние от исходного местоположения. Оптимизация уровня 2 обеспечивает хороший выигрыш по быстродействию, но может значительно увеличить время компиляции некоторых программ.
  •  PLSQL_0PTIMIZE_LEVEL = 3 — этот уровень оптимизации, появившийся только в Oracle11g, включает возможность подстановки (inlining) кода вложенных или локальных подпрограмм. Он может принести пользу в особых случаях (при большом количестве локальных подпрограмм или рекурсивных вызовов), но для большинства приложений PL/SQL должно хватить используемого по умолчанию уровня 2. Уровень оптимизации можно задать для экземпляра в целом, а затем переопределить его для конкретного сеанса или программы. Пример:
ALTER SESSION SET PLSQL_0PTIMIZE_LEVEL = 0;

Oracle поддерживает настройки оптимизатора на уровне отдельных модулей. При перекомпиляции модуля с настройками, отличными от настроек по умолчанию, новые значения «закрепляются», что позволяет позднее использовать их конструкцией REUSE SETTINGS:

ALTER PR0CEDURE bigproc C0MPILE PLSQL_0PTIMIZE_LEVEL = 0;

и затем:

ALTER PR0CEDURE bigproc C0MPILE REUSE SETTINGS;

Чтобы просмотреть все настройки компилятора для ваших модулей, включая уровень оптимизации, режим (интерпретация или код для машинной платформы) и уровни предупреждений компилятора, обратитесь с запросом к представлению USER_PLSQL_ OB3ECT_SETTINGS.

 

Как работает оптимизатор PL/SQL

Оптимизаторы не только могут делать то, что запрещено обычным программистам, — они также способны выявлять и использовать закономерности в вашем коде, которые могут остаться незамеченными для вас. Один из главных приемов, применяемых оптимизаторами, заключается в изменении порядка выполняемых операций для повышения эффективности исполнения. Определение языка программирования устанавливает границы для перестановок, которые могут выполняться оптимизатором, но определение PL/SQL оставляет немалую свободу действий для оптимизатора. В оставшейся части этого раздела обсуждаются некоторые возможности PL/SQL с приведением примеров кода, который может выиграть от их применения.

Для начала рассмотрим пример с инвариантом цикла — операцией, которая выполняется в цикле, но остается неизменной во всех итерациях. Любой хороший программист взглянет на следующий фрагмент:

FOR e IN (SELECT * FROM employees WHERE DEPT = p_dept)
LOOP
   DBMS_OUTPUT.PUT_LINE('<DEPT>' || p_dept || '</DEPT>');
   DBMS_OUTPUT.PUT_LINE('<emp ID="' || e.empno || '">'); 
   etc.
END LOOP;

и скажет вам, что код будет выполняться быстрее, если вывести «инвариант» из цикла для предотвращения его лишних выполнений:

l_dept_str := '<DEPT>' || p_dept || '</DEPT>'
FOR e IN (SELECT * FROM employees WHERE DEPT = p_dept)
LOOP
   DBMS_OUTPUT.PUT_LINE(l_dept_str);
   DBMS_OUTPUT.PUT_LINE('<emp ID="' || e.empno || '">'); 
   etc.
END LOOP;

Впрочем, даже хороший программист может решить, что ясность кода первой версии предпочтительнее эффективности второй. Начиная с Oracle Database 10g PL/SQL уже не заставляет вас выбирать между ними. Со стандартными настройками оптимизатора компилятор выявляет закономерность в первой версии и преобразует ее в байт-код, реализующий вторую версию. Это возможно благодаря тому, что определение языка не требует многократного выполнения инвариантов в циклах; это одна из возможностей, которую оптимизатор может использовать (и использует). Возможно, вам кажется, что эффект от такой оптимизации незначителен, но даже мелочи способны накапливаться. Я еще не видел ни одной базы данных, которая бы становилась меньше со временем. Многие программы PL/SQL перебирают все записи растущей таблицы, а таблица с миллионом строк уже не считается чем-то необычным. Лично я буду очень рад, если Oracle автоматически устранит миллион лишних операций из моего кода.

Другой пример — рассмотрим следующую серию команд:

result1 := r * s * t;
...
result2 := r * s * v;

Если значения r и s не могут изменяться между этими двумя командами, PL/SQL может откомпилировать код в следующем виде:

interim := r * s; 
resultl := interim * t;
...
result2 := interim * v;

Оптимизатор поступит так, если он решит, что сохранение значения во временной переменной будет выполнено быстрее, чем повторное умножение.

Oracle делится этой и другой информацией об оптимизаторе PL/SQL в статье «Freedom, Order, and PL/SQL Compilation», доступной в Oracle Technology Network (введите название статьи в поле поиска). Основные положения статьи:

  •  Если ваш код не требует выполнения фрагмента в строго определенном порядке по правилам ускоренного вычисления выражений или следования команд, PL/ SQL может выполнить фрагмент в порядке, отличном от порядка его написания. Проявления этого изменения порядка могут быть разными: в частности, оптимизатор может изменить порядок выполнения инициализационных разделов пакета, а если вызывающей программе требуется только доступ к константе из пакета — компилятор просто сохраняет эту константу в вызывающем коде.
  •  PL/SQL рассматривает вычисление индексов массивов и идентификацию полей записей как операторы. Если у вас имеется вложенная коллекция записей и вы обращаетесь к определенному элементу и полю (например, price(product)(type).settle), PL/SQL должен определить внутренний адрес, связанный с переменной. Этот адрес интерпретируется как выражение; он может быть сохранен и использован позднее в программе для предотвращения затрат, связанных с повторными вычислениями.
  •  Как было показано ранее, PL/SQL может вводить промежуточные значения для предотвращения вычислений.
  •  PL/SQL может полностью исключать некоторые операции (например, x*0). При этом явный вызов функции не исключается; в выражении f()*0 функция f() всегда будет вызываться при наличии побочных эффектов.
  •  PL/SQL не вводит новые исключения.
  •  PL/SQL может снять необходимость в инициировании исключений. Например, исключение деления на 0 в следующем коде может быть удалено, потому что оно недостижимо:
IF FALSE THEN y := x/0; END IF;

PL/SQL не предоставляет возможности сменить обработчик для заданного исключения.

Пункт 1 заслуживает более подробно рассмотрения. В написанных мной приложениях я привык пользоваться разделами инициализации пакетов, но я никогда не беспокоился о порядке их выполнения. Мои инициализационные разделы обычно были небольшими, и в них выполнялось присваивание статических значений (обычно читаемых из базы данных); эти операции не зависели от порядка выполнения. Если ваше приложение должно гарантировать порядок выполнения, лучше вывести код из инициализационного раздела и поместить его в раздельные функции инициализации, вызываемые явно. Например, вызов может выглядеть так:

pkgA.init();
pkgB.init();

В этом случае сначала выполняется инициализация pkgA, а затем инициализация pkgB. Данная рекомендация остается истинной, даже если вы не используете оптимизирующий компилятор.

О пункте 2 с примером price(product) (type) .settle тоже стоит упомянуть особо. Если эта ссылка используется многократно с разными значениями переменной type, но одинаковыми значениями переменной product, оптимизация может разбить адресацию на две части — первая вычисляет price(product), а вторая (используемая в нескольких разных местах) вычисляет оставшуюся часть. Такой код выполняется быстрее, потому что при каждом использовании ссылки вычисляется только переменная часть адреса. Еще важнее другое — такие изменения легко вносятся компилятором, но из-за семантики PL/SQL внести их программисту в исходном коде будет очень трудно. Большая часть изменений оптимизации относится к этой категории; компилятор «за кулисами» легко делает то, что будет трудно сделать программисту.

PL/SQL включает другие возможности для выявления и ускорения некоторых идиом программирования. В команде

counter := counter + 1;

компилятор не генерирует машинный код полного сложения. Вместо этого PL/SQL распознает идиому программирования и использует специальную команду виртуальной машины PL/SQL (PVM) — «инкремент». Эта команда выполняется намного быстрее традиционного сложения (это относится к подмножеству числовых типов данных — PLS_INTEGER и SIMPLE_INTEGER, но не к NUMBER).

Также существует специальная команда для кода, выполняющего конкатенацию нескольких операндов:

str := 'valuel' || 'value2' || 'value3' ...

Вместо того чтобы рассматривать происходящее как серию попарных конкатенаций, компилятор и PVM совместно выполняют серию конкатенаций в одной команде. Большая часть замен, выполняемых оптимизатором, остается незамеченной для вас. Во время обновления может обнаружиться программа, которая работает хуже, чем вы рассчитывали, — потому что она полагается на порядок выполнения, который был изменен компилятором. Скорее всего, проблемы следует искать в порядке инициализации пакетов, но конечно, у вас все может быть по-другому.

И последнее замечание: способ модификации кода оптимизатором детерминирован, по крайней мере для заданного значения PLSQL_0PTIMIZE_LEVEL. Другими словами, если вы пишете, компилируете и тестируете свою программу — скажем, с принятым по умолчанию уровнем оптимизации 2, ее поведение не изменится с переходом на другой компьютер или другую базу данных — при условии, что версия базы данных и уровень оптимизации остались неизменными.

 

Оптимизация циклов выборки данных

Для версий до Oracle9i Database Release 2 включительно цикл FOR с курсором наподобие приведенного ниже будет получать ровно одну логическую строку за операцию выборки:

FOR arow IN (SELECT something FROM somewhere)
   ...
LOOP
END LOOP;

Итак, если команда должна получить 500 строк, операция выборки будет выполнена 500 раз, что потребует 500 высокозатратных «переключений контекста» между PL/ SQL и SQL.

Однако начиная с Oracle Database 10g база данных выполняет автоматическую «массовую выборку», так что каждая выборка получает (до) 100 строк. Предыдущий цикл FOR с курсором использует только 5 операций выборки для получения 500 строк от ядра SQL. Все выглядит так, словно база данных автоматически перепрограммирует цикл с использованием конструкции BULK COLLECT.

Эта явно недокументированная функция также работает с кодом в следующей форме:

FOR arow IN cursorname
LOOP
   ...
END LOOP;

Однако она не будет работать для кода в следующей форме:

OPEN cursorname;
LOOP
   EXIT WHEN cursorname%NOTFOUND;
   FETCH cursorname INTO ...
END LOOP;
CLOSE cursorname;

Тем не менее эта внутренняя оптимизация обеспечивает большой выигрыш для циклов FOR с курсором (дополнительным преимуществом которых является компактность).

 

Вас заинтересует / Intresting for you:

Управление приложениями PL/SQL...
Управление приложениями PL/SQL... 4651 просмотров Stas Belkov Thu, 16 Jul 2020, 06:20:48
Встроенные методы коллекций PL...
Встроенные методы коллекций PL... 14849 просмотров sepia Tue, 29 Oct 2019, 09:54:01
Средства оптимизации приложени...
Средства оптимизации приложени... 8090 просмотров Александров Попков Fri, 16 Nov 2018, 15:27:51
Программирование динамического...
Программирование динамического... 4922 просмотров Максим Николенко Sun, 09 Sep 2018, 06:56:23
Войдите чтобы комментировать

OraCool аватар
OraCool ответил в теме #9508 4 года 6 мес. назад
При обновлении Oracle новые возможности компилятора PL/SQL необходимо тщательно протестировать. В некоторых (достаточно редких) случаях переход на Oraclellg сопровождается небольшими изменениями в порядке выполнения (в соот­ветствии с решениями, принимаемыми оптимизирующим компилятором), которые могут повлиять на результаты приложения. Спасибо за статью!