пятница, 15 апреля 2011 г.

Сравнение производительности процедур в PL/SQL

Каждому программисту, наверное, рано или поздно приходится встречаться с проблемой выбора: какой алгоритм из двух (или более) выбрать в качестве оптимального? Ну например, есть две процедуры, результат работы которых одинаков, но реализованы процедуры по разному. При решении проблемы выбора, весомым аргументом является скорость выполнения процедуры.

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

А дело было примерно так. Написал я тест: сначала запускается процедура А в цикле (измеряется время до выполнения и после), потом, в этом же тесте, запускается в цикле процедура Б (и измеряется время). И вот результат меня удивил: я предполагал, что процедура А должна была отработать чуть быстрее Б, но тест показывал обратное.

Не понимая, в чём же дело, я, по наитию, поменял местами вызовы процедур, т.е. сначала Б, затем А. И результат изменился на противоположный.

Почему такое происходит – я не знаю. PL/SQL – язык интерпретируемый, возможно, перед вызовом первой процедуры, часть времени уходит на выделение памяти, а при вызове второй процедуры, используется уже выделенная память. Может быть дело в чём-то другом.

В общем, поэкспериментировав, у меня получился такой шаблон для измерения производительности PL/SQL кода:

declare
t timestamp := localtimestamp;
chk_cnt_ integer := 10;
tst_cnt_ integer := 10000;

procedure done(msg_ in varchar2 default null) is
begin
if msg_ is not null then
dbms_output.put_line(to_char(localtimestamp - t)||' '||msg_);
end if;
t := localtimestamp;
end;

procedure test1
is
date_ date;
begin
date_ := sysdate;
end;

procedure test2
is
date_ date;
begin
select sysdate
into date_
from dual;
end;

begin
for i in 1 .. chk_cnt_ loop
test1;
end loop;
done;

for i in 1 .. tst_cnt_ loop
test1;
end loop;
done('tst 1');

--
for i in 1 .. chk_cnt_ loop
test2;
end loop;
done;

for i in 1 .. tst_cnt_ loop
test2;
end loop;
done('tst 2');
end;

Здесь: chk_cnt_ – кол-во “холостых” циклов, время выполнения которых не учитывается, tst_cnt_ – кол-во циклов, для измерения времени выполнения процедур. Чем сложнее код тестируемой процедуры, тем меньше стоит указывать это число (иначе долго придётся ждать). Процедура done выводит в output время выполнения тест-цикла.


В приведённом примере test1 и test2 никакой практической ценности не представляют, процедуры написаны просто для примера и показывают, что обращение к SQL из PL/SQL заметно медленнее.

2 коммент.:

Юрий комментирует...

Жалко что цифры вы не привели, интересно

glhsmile комментирует...

Нее парень :-) так не пойдет измерять производительность.

Так ты тестируешь не производительность своих процедур, а грамотность программистов Oracle. :-)

> Почему такое происходит – я не знаю.
Внутренние кэши заполняются.
Код вызывается чаще - становиться со временем "ближе", "доступнее" быстрее извлекается.
Тоже и с данными.
Нужно сбрасывать буфера сессии.
Не помню как, к сожалению, но грамотный dba должен подсказать.
Я не то, что не грамотный, просто ну ни разу не oracle dba. Я dbu :-)
Но точно знаю, что простое хронометрирование эффекта не дает.

А первое, что нужно смотреть это таки план исполнения SQL.

> показывают, что обращение к SQL из PL/SQL заметно медленнее.

Не, немного перефразировать надо.
SQL исполняет одна виртуальная машинка.
PL/SQL совсем другая, именно потому функции, что допустимы в SQL не доступны в процедурном расширении, например nvl2.
Соответственно, когда происодит смешивание кода происходит ИЗМЕНЕНИЕ КОНТЕКСТА исполнения кода.
А это ДОРОГАЯ операция.
Код внутри test1 исполняется той-же VM, что и вызывающий его цикл.
А при исполнении test2 происходит смена контекста, а значит сохранение состояния VM.
Отсюда потери.

declare
iCOUNT CONSTANT INTEGER := 9999999;
i integer;
t NUMBER;
s STRING(96);
begin
t := DBMS_UTILITY.GET_TIME;
FOR i IN 0..iCOUNT LOOP
s := LPAD(i,7);
END LOOP;
DBMS_OUTPUT.PUT_LINE('I='||s||' LPAD ='||to_char(DBMS_UTILITY.GET_TIME-t));

t := DBMS_UTILITY.GET_TIME;
FOR i IN 0..iCOUNT LOOP
s := to_char(i,'0000000');
END LOOP;
DBMS_OUTPUT.PUT_LINE('I='||s||' to_char ='||to_char(DBMS_UTILITY.GET_TIME-t));

end;

Отправить комментарий

.

.