Реализация оптимизирующих преобразований программ с помощью структурных предикативных грамматик

МАТЕМАТИКА

УДК 519.68

РЕАЛИЗАЦИЯ ОПТИМИЗИРУЮЩИХ ПРЕОБРАЗОВАНИЙ ПРОГРАММ С ПОМОЩЬЮ СТРУКТУРНЫХ ПРЕДИКАТИВНЫХ ГРАММАТИК

© 2006 г С.П. Крицкий, Б.Ю. Тапкинов

In this article the structural predicative grammars are described. They are offered as a tool for construction and analysis of the intermediate representation of programs (the structural graph) and also for implementation of optimizing transformations of programs.

Для повышения эффективности оптимизирующих компиляторов используется метод, основанный на переводе входной программы в некоторую промежуточную форму, которая позволяет более успешно осуществлять потоковый анализ и реализовывать оптимизирующие преобразования. Примером такого промежуточного представления может являться абстрактное атрибутированное синтаксическое дерево программы [1]. Авторами в качестве промежуточного представления программы для реализации оптимизирующих преобразований был выбран структурный граф [2], являющийся результатом синтаксического и контекстного анализа с помощью структурной предикативной грамматики (СП-грамматики) [2] и используемый для построения управляющего графа, графа зависимо -стей по данным, графа вычислений и других графов, применяемых для оптимизации программ. В статье на примере простого модельного языка демонстрируется методика построения структурного графа и графа зависимостей по данным, а также реализация оптимизирующего преобразования (удаление бесполезных операторов).

1. Структурные предикативные грамматики

Структурные предикативные грамматики (СП-грамматики) являются расширением DC-грамматик (Definite Clause Grammars) [3, 4] декларативными средствами определения и анализа семантической структуры в виде графа, в основе которого лежит семантическое дерево программы. Ядром этого расширения является определяемое ниже понятие структурного графа и процедура унификации вершин такого графа и термов.

Понятия структурного графа и СП-грамматики используют определение многосортного языка первого порядка [5], которое мы приводим ниже.

Язык первого порядка задаётся сигнатурой Q = (S, Fn, Pr), где S -непустое множество символов (имён), называемых сортами; Fn - множество функциональных символов, для каждого из которых определён тип sb ..., sn ^ s (сорта аргументов и результата n-арной функции, n > 0;

0-арная функция называется константой сорта •); Рг - множество предикатных символов, для каждого из которых определён тип 5Ь ..(сорта аргументов п-арного предиката). С каждым сортом связано счётное множество У^ символов индивидных переменных (или просто переменных) сорта •.

Для каждого языка первого порядка стандартным образом определяются термы и формулы:

Термом сорта • называется либо переменная, либо константа сорта либо конечное выражение вида/(/ь ..., 4), где/ - функциональный символ типа 5Ь • •п ^ а ^ - терм сорта si .

Атомарной формулой называется выражение вида р (/ь ..., 4), где р -предикатный символ типа 5Ь .••, •п, а ^ - терм сорта Остальные формулы языка (логические) строятся из атомарных с помощью обычных логических связок и кванторов.

Чтобы говорить о значениях термов и истинности или ложности формул, необходимо задать:

1) интерпретацию языка 1(Щ в виде

- множества объектов Б, каждый из которых является объектом некоторого сорта (возможно, нескольких), причём для каждого сорта • множество объектов этого сорта не пусто; множество называется носителем сорта •, оно является областью изменения переменных сорта •;

- множества функций на Б, причём каждый функциональный символ типа •1, ..., •п ^ • обозначает некоторую функцию, задающую отображение Б х...х Б ^ Б•;

- множества отношений (предикатов) на Б, причём каждый предикатный символ типа •1, ..., •п обозначает некоторое подмножество множества Б_ х...хБ_ ;

•1 •п

2) оценку (означивание) переменных, входящих в терм или формулу, т.е. функцию, определяющую в качестве значения каждой переменной сорта • некоторый объект того же сорта.

Во многих приложениях языков первого порядка (в том числе и в БС-грамматиках) используется такая интерпретация, в которой объектами некоторого сорта являются сами термы этого сорта. В этих приложениях при решении уравнений вида ^ = /2 используется алгоритм унификации термов для нахождения наименьшего унификатора - таких значений входящих в уравнение переменных, при подстановке которых уравнение становится тождеством [1].

В СП-грамматиках для описания «семантической» структуры программы используются языки первого порядка и их интерпретации, в которых объектами являются термы, образованные специальными функциями-конструкторами. Все сорта делятся на первичные и конструируемые, а среди функциональных символов выделяются конструкторы, результат

которых должен быть конструируемого сорта. При этом объекты конструируемых сортов образуются только с помощью конструкторов. Множество образованных таким образом термов Тт используется в правилах грамматики в качестве аргументов терминальных и нетерминальных символов, как и в БС-грамматиках. Однако, в отличие от последних, механизм синтаксического разбора в СП-грамматиках обеспечивает построение структурного графа анализируемой программы.

Структурным графом называется конечный ориентированный упорядоченный граф, удовлетворяющий следующим условиям:

1. Каждая вершина графа помечена либо объектом первичного сорта, либо конструктором, либо переменной; сорт метки считается сортом вершины; две вершины не могут помечаться одной переменной.

2. Дуги графа соответствуют аргументам конструкторов, т.е. выходят только из вершин, помеченных «-местным конструктором, где п > 0; если тип конструктора/ 51, ..., ^ 8, то из вершины/выходит п дуг и ¡-я дуга входит в вершину у, сорта (вершины уь...,уп не обязательно различны). В этом случае вершину/можно обозначать_Дуь ..., уп).

Множество Тт расширим, допустив в качестве терма сорта 5 использование любой вершины структурного графа сорта 5. Для этого множества термов расширим алгоритм унификации для случая унификации вершины графа и терма из Тт. В этом случае алгоритм унификации пытается, начиная с заданной вершины, развернуть граф в дерево терма, что может привести к перестройке графа. Вершины-переменные являются «точками роста» графа.

Рекурсивный алгоритм унификации описан в таблице.

Алгоритм унификации

Вершина графа Подтерм Результат

1. /(У1, , Уп) /(¡1, , 4) Переход к последовательной унификации у1 и и т.д. При этом найденные на каждом шаге подстановки применяются ко всему терму

2. Объект первичного сорта или 0-местный конструктор Тот же объект или конструктор Унификация вершины успешно завершена

3. Вершина Та же вершина

4. Вершина у сорта 5, не являющаяся переменной Переменная X сорта 5 Все вхождения X заменяются на у; в графе вершины X и у отождествляются. При отождествлении вершин в графе они отождествляются и в терме

5. Переменная X сорта 5 Вершина у того же сорта

6. Переменная X сорта 5 Переменная У сорта 5 Отождествление (переименование) всех вхождений этих переменных. В графе это может привести к отождествлению вершин X и У

Окончание таблицы

Вершина графа Подтерм Результат

7. Переменная X сорта s Терм Т сорта не являющийся вершиной или переменной (X может входить в Т) Вершины дерева терма Т и графа, помеченные ранее переменной X, отождествляются с корнем дерева терма Т. Отождествляются и другие вершины полученного графа, помеченные одной переменной. Остальные вершины дерева, не являвшиеся вершинами графа, становятся новыми вершинами графа

8. В остальных случаях унификация невозможна

Унификация вершин структурного графа и термов используется в выводе по грамматике. Для каждого нетерминального символа грамматики должно быть указано, какие его параметры (термы из Tm) должны унифицироваться с вершинами графа. Ниже мы будем эти параметры выделять в левой части правил полужирным шрифтом. Если в цели p(X), сопоставляемой с заголовком правила p(T):-..., символ X - обычная некон-кретизированная переменная (не вершина графа), то должна быть создана изолированная вершина-переменная X. После этого она будет унифицироваться с термом T по указанным выше правилам.

Вывод в СП-грамматиках определяется так же, как в DC-грамматиках, с тем только дополнением, что кроме унификации термов используется унификация вершин структурного графа и термов. Эта новая унификация обозначается семантическим предикатом ~ в тех случаях, когда нужно выполнить её явно.

Пример 1. Данная структурная предикативная грамматика порождает программы некоторого языка, который будет использоваться в дальнейших примерах. В правилах грамматики используется нотация DC-грамматик.

Программа представляет собой блок, ограниченный операторными скобками begin, end и состоящий из описания переменных, операторов присваивания, вызова подпрограмм и вложенных блоков. Отношение B1 << B обозначает непосредственную вложенность блока B1 в блок B.

Первичные сорта:

Id - идентификаторы;

R - числа. Конструируемые сорта:

E - выражения;

S - операторы;

SC - композиции операторов;

V - списки переменных;

L - списки выражений. Конструкторы:

bl: V,SC -> S - блок операторов;

[ ]: S, SC -> SC - композиция операторов;

[ ]: Id, V -> V [ ]: E, L -> L call: Id,L -> S let: Id, E -> S null: -> S null: -> V '+', '*': E,E -> E n: R -> E I: -> E

- список переменных;

- список выражений;

- оператор вызова подпрограммы;

- оператор присваивания;

- пустой оператор;

- пустой список переменных;

- арифметические операции;

- число;

- для любого идентификатора I в синтаксическом вы-

ражении.

Правила грамматики:

% построение вершины блока со списком переменных и списком операторов Ыоскф) -->

^т],{Б = Ы(У,8С)},уапаЫе8(У),8Шешеп18(8С,Б),[еМ]. % построение списка переменных уалаЪ!^^]) -->

[уаг, 1а(Ы)], {I = Ы}, уаге(У), ^еагЛ(Ы,У)}. уаге(Р^]) -->

",",^(Ы)], {I = И}, уаге(У), {seaгch(Id,У)} уата^^^ -->

% построение списка операторов statements([S|SC],B) --> stat(S,B),stats(SC,B). stats([S|SC],B) --> ";",stat(S,B),stats(SC,B). % пустой оператор stats(null,B) -->

% оператор вызова подпрограммы stat(caЩI,L),B) -->

^(Ы^Х],^ = Id},act_params_list(L,B), ")". % оператор присваивания stat(let(I,E),B) -->

[id(Id)], ":=",{def(Id,Б,I)},expг(E,Б). % вложенный блок операторов stat(B1,B) -->

^т],{Б1 = Ъ1(У,БС),=>(Б1 << Б))},уaгiaЪles(У),stateшents(SC,Б1),[end]. % построение выражения ехрг^З) -->

teгш(E1,Б),teгшs(E1,E2,Б). teгш(E,B) -->

factor(E1,B),factors(E1,E,B).

factor(n(R),B) --> [n(N)],{R = N}. % выражение в скобках factor(E,B) -- > " (",expr(E,B), ")".

% число

% переменная factor(I,B) --> [id(Id)],{def(Id,B,I)}. terms(E1,E,B) -->

"+",term(E2,B),terms('+'(E1,E2),E,B).

terms(E,E,B) --> !!!!

factors(E1,E,B) -->

"*",factor(E2,B),factors('*'(E1,E2),E,B).

factors(E,E,B) --> !!!!

% параметры подпрограммы

act_params_list([E|L],B) -->

expr(E,B),expressions(L,B).

expressions([E|L],B) -->

",",expr(E,B),expressions(L,B).

expressions(null,B) --> !!!!

В данной грамматике используются предикаты search и def, определения которых не приводятся, а их смысл состоит в следующем. Предикат search(Id,V) используется для проверки повторного описания переменной, т.е. он возвращает истину, если Id не входит в список V. Предикат def(Id,B,I) ищет Id в списке описанных переменных текущего блока B, и если переменная Id присутствует в этом списке, т.е. существует некоторая вершина X, помеченная Id, то унифицирует I с вершиной X. Если в текущем блоке переменная Id не описана, то поиск происходит во внешнем блоке и т. д. Если переменная Id не найдена ни в одном из блоков, то выводится сообщение об ошибке.

Пример 2. На рис. 1 представлен структурный граф, полученный в процессе разбора с помощью предыдущей СП-грамматики программы

Рис. 1

(0) begin

var a, b;

(1) a := 1;

(2) b := 2;

(3) a := a + b;

(4) b := a - b;

(5) a := a - b;

(6) end.

2. Построение графа зависимостей по данным по структурному графу программы

Рассмотрим линейный участок программы, состоящий из последовательности операторов Si, Si+1, ... Sn ^ < п). Для данного линейного участка построим граф зависимостей по данным (ГЗД) [1]. Приведем пример. Для следующей программы:

(0) begin

var i, b, x, a, k;

(0) i := 4;

(0) b := 9;

(0) x := b * 8;

(0) a := i * b;

(0) i := 5 + x;

(0) k := a + i;

(0) write(k);

(0) end

ГЗД представлен на рис. 2.

- потоковая (истинная) зависимость

- антизависимость

- выходная зависимость

Рис. 2

Для удобства реализации ГЗД будем представлять списком его дуг. Дуга потоковой зависимости, проведенная из вершины X в вершину У, представлена фактом агс_ш_оШ:(Х, У) в базе данных Пролога [6], где X, У -ссылки на операторы в структурном графе программы. Аналогично дуги антизависимости и выходной зависимости представлены, соответственно, фактами: агс_ои_ш(Х, У) и агс_ои_оШ:(Х, У). Например, для фрагмента программы, приведенного выше, ГЗД будет представлен набором фактов: агс_т_ои^6,7), агс_т_ои(5,6), агс_т_о1и;(4,6), агс_ои_т(4,5), агс_т_ои(3,5), агс_ои_ои(1,5), агс_т_ои^2,4), агс_т_ои(1,4), агс_т_ои^2,3).

Список определений - цепочка в БД Пролога, состоящая из термов вида def(Id, S), где S - вершина, помеченная некоторым оператором, а Ы -вершина, помеченная переменной, определяемой этим оператором.

Список использований - цепочка в БД Пролога, состоящая из термов вида ше(Ы, 8), где 8 - вершина структурного графа программы, помеченная некоторым оператором; И - вершина, помеченная переменной, используемой этим оператором.

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

Список определений: <!е£(к,6), (е£(1,5), (1е1"(а,4), (е£(х,3), йе^ЬД), йеЩД). Список использований: ше(к,7), ше(1,6), ше(а,6), ше(х,5), ше(Ь,4), ше(1,4), ше(Ь,3).

Приведенный ниже алгоритм предназначен для линейных программ, состоящих только из операторов присваивания и вызова подпрограмм. Операторы программы нумеруются от 1 до п.

Алгоритм. Построение ГЗД.

(0) начало

(1) 1:= 1;

(2) Рассматриваем оператор Б1 и находим все его входные переменные:

Хь Х2, ..., Хт;

(3) Если входных переменных нет (т = 0), то переход на шаг 10;

(4) j := 1;

(5) Если переменная Xj определялась оператором Бк (к < 1), то переход на шаг 6, иначе - переход на шаг 7;

(6) Добавляем в ГЗД О дугу потоковой зависимости (Бк, Б1), т.е. добавляем в БД факт агс_1п_ои11(8к, Б1);

(7) Добавляем в список использований элемент ше^, 1);

(8) j :=j + 1;

(9) Если j > т, то переход на шаг 10, иначе - переход на шаг 5;

(10) Если Б1 - оператор присваивания вида У := Е(ХЬ Х2, ..Хт), то переход на шаг 11, иначе - переход на шаг 16;

(11) Находим по списку определений последний по исполнению оператор

(И < 1), определивший переменную У;

(12) Добавляем в ГЗД О дугу выходной зависимости (Бь Б1), т.е. добавляем в БД факт агс_ои1_ои1(8ь, Б1);

(13) Находим по списку использований последний по исполнению оператор ^ < 1), использовавший переменную У;

(14) Добавляем в ГЗД О дугу антизависимости Б1), т.е. добавляем в БД факт агс_ои1_1п(8ч, Б1);

(15) Добавляем в список определений элемент йе£(У, 1);

(16) 1 := 1 + 1;

(17) Если 1 > п, то переход на шаг 18, иначе - переход на шаг 2;

(18) конец.

Рассмотрим реализацию алгоритма на Прологе. Предикат analyze(R), где R - ссылка на оператор в структурном графе программы, выполняет следующие задачи:

1. Формирует список входных переменных оператора.

2. По этому списку с помощью предиката add_arcs:

2.1. в ГЗД добавляются необходимые дуги потоковой зависимости;

2.2. для каждой входной переменной Xi в список использований добавляется элемент ше(Хъ Я).

3. Если Я - ссылка на оператор присваивания, определяющий переменную У, то в список определений добавляется элемент def(Y, Я).

4. Если Я - ссылка на блок операторов, то рекурсивно анализируются операторы, входящие в это блок.

Анализ вершин структурного графа осуществляется с помощью унификации вершины и терма.

analyze(null). analyze(bl(V,S)) :-analyze_st_comp(S).

analyze(R) :-R = call(_,L), get_expr_vars_list(L,L 1), add_arcs(R,L1).

analyze

% пустой оператор пропускаем % блок операторов

% рекурсивный анализ всех операторов, входящих в блок

% оператор вызова подпрограммы % получение из списка параметров всех % используемых переменных и добавление % в ГЗД необходимых дуг потоковой зависимости

% оператор присваивания

% формируем список используемых переменных % добавляем в ГЗД дуги потоковой зависимости % ищем по списку определений оператор Я1, определивший

% переменную V, и строим дугу выходной зависимости из Я1 в Я

гecoгded(uses,use(V,R1),_), % ищем по списку использований оператор Я1, использовавший

% переменную V, и строим дугу антизависимости из Я1 в Я

assert

assert

% добавляем в список определений элемент

¿е/^Я)

% анализ последовательности операторов

; true ),

recorda(defs,def(V,R),_).

analyze_st_comp([S|SL]) :-analyze(S), analyze_st_comp(SL). analyze_st_comp(null). add_arcs(R, [E|L]) :-

recorda(uses,use(E,R),_), % добавляем в список использований элемент

use

(recorded(defs,def(E,R1),_), % ищем по списку определений оператор R1, определивший

assert(arc_in_out(R1,R)) % переменную E, и строим дугу потоковой зависимости из R1 в R

; true

),

add_arcs(R, L). % продолжаем анализ списка переменных

add_arcs(R, []).

3. Удаление бесполезных операторов

В качестве иллюстрации применения структурного графа для оптимизации программ рассмотрим известное преобразование, состоящее в удалении бесполезных операторов [7].

Последовательность операторов Si; Si + 1; ...; Sj является областью действия [7] оператора Si, если выполнены следующие два условия для переменной х, являющейся результатом Si:

- х не является результатом ни одного из операторов Si+1; ...; SJ-1;

- либо Sj - оператор останова (конца программы), либо х является результатом оператора Sj.

Бесполезный оператор - оператор, присваивающий значение переменной, которая не используется в области действия этого оператора [7]. В ГЗД бесполезным операторам будут соответствовать вершины, не содержащие исходящих дуг потоковой зависимости.

Рассмотрим линейную программу:

(0) begin

var x, i, r, c, b, a, k;

(1) x := 1;

(2) i := 5 + x;

(3) r := i;

(4) c := 45;

(5) b := c / 9;

(6) a := i * b;

(7) r := b + 1;

(8) i := 5 + x;

(9) b := 4 * i;

(10) a := c * r;

(11) r := b + 1;

(12) k := a + i * r;

(13) write(k);

(14) end.

ГЗД для этой программы представлен на рис. 3.

Так как в области действия оператора (3) г := 1 (операторы 3-7) переменная г не используется в качестве входной ни одним из операторов, то оператор (3) может быть удален. По этой же причине может быть удален и оператор (6). Однако удалять оператор (5) нельзя, так как его результат (переменная Ь) используется в операторах (6) и (7). Нельзя удалять и оператор (13), поскольку он только использует, а не определяет переменную к.

Краткая схема алгоритма удаления бесполезных операторов:

1. Строится ГЗД.

2. По построенному ГЗД определяются вершины Sь S2, ..., Sn, не содержащие исходящих дуг потоковой зависимости.

3. По найденным вершинам определяются, а затем удаляются бесполезные операторы (в структурном графе соответствующие вершины помечаются пустым оператором).

В следующем алгоритме удаляются бесполезные операторы, которые были найдены только в исходной программе. Операторы, ставшие бесполезными после преобразования программы, не удаляются. Алгоритм. Удаление бесполезных операторов.

(0) начало

(0) i := 1;

(0) рассматриваем оператор

(0) Если Si - оператор присваивания и в ГЗД не существует дуг потоковой зависимости вида то в структурном графе заменяем оператор Si на пустой, а в ГЗД удаляем все дуги потоковой зависимости вида (_Д);

(0) i := i + 1;

(0) если i <= п, то на шаг 2;

(0) конец.

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

run_proc_for_list([S|SL]) :- % анализируем последовательность операторов unuseful_stat(S), % проверка оператора на «бесполезность»

run_proc_for_list(SL). % продолжаем анализ...

гun_pгoc_foг_list(null).

% если это блок операторов, то % анализируем вложенные операторы

% если это оператор присваивания, то % проверяем отсутствие исходящих дуг потоковой зависимости % заменяем на пустой оператор % удаляем все входящие дуги потоковой зависи-

unuseful_stat(bl(S)) :-run_proc_for_list(S).

unuseful_stat(R) :-R = let(_,_), not(arc_in_out(R,_)),!,

replace(R,null), repeat,

мости

not(retract(arc_in_out(_,R))). unuseful_stat(_).

Литература

Касьянов В.Н., Евстигнеев В.А. Графы в программировании: обработка, визуализация и применение. СПб., 2003.

Крицкий С.П. // Компьютерное моделирование. Вычислительные технологии. Ростов н/Д, 2003. С. 67-90.

PereiraF., Warren D. // Artificial Intelligence. 1980. Vol. 13. P. 231-278. Стерлинг Л., Шапиро Э. Искусство программирования на языке Пролог. М., 1990. Колмогоров А.Н., Драгалин А.Г. Математическая логика. М., 2005. The Arity/Prolog Language reference manual. Concord, 1988. Касьянов В.Н. Оптимизирующие преобразования программ. М., 1988.

Ростовский государственный университет

14 октября 2005 г.

УДК 519.673

НЕКОТОРЫЕ КРАЕВЫЕ ЗАДАЧИ, НЕЛОКАЛЬНЫЕ ПО ВРЕМЕННОЙ ПЕРЕМЕННОЙ, ДЛЯ УРАВНЕНИЯ ЧЕТВЕРТОГО ПОРЯДКА

© 2006 г.

А.А. Керефов , Е.В. Плотникова

In the article by using differential and constructional behaviors of Riemann function for the forth-order equation one-valued solvability in non-local series by time variable in boundary problem are established.

Рассмотрим прямоугольник D = {(х, t): 0< х< I, 0< t< T}; D - замыкание области D; обозначим через Cm,n (D) множество функций и(х, t), определенных и непрерывных в D вместе со своими частными производ-

dku

ными вида-, где i = 0, 1, ... m; j = 0, 1, ... n; k = i + j.

дх'дtj

Множество Cm,n (D) с нормой