Концепция удаленного вызов процедур (RPC)

 

Идея вызова удаленных процедур (Remote Procedure Call - RPC) состоит в расширении хорошо известного и понятного механизма передачи управления и данных внутри программы, выполняющейся на одной машине, на передачу управления и данных через сеть. Средства удаленного вызова процедур предназначены для облегчения организации распределенных вычислений. Наибольшая эффективность использования RPC достигается в тех приложениях, в которых существует интерактивная связь между удаленными компонентами с небольшим временем ответов и относительно малым количеством передаваемых данных. Такие приложения называются RPC-ориентированными.

Характерными чертами вызова локальных процедур являются:

· Асимметричность, то есть одна из взаимодействующих сторон является инициатором;

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

Этапы выполнения RPC

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

Затем параметры должны быть преобразованы в соответствующий формат и вставлены в буфер сообщения. К этому моменту сообщение готово к передаче, поэтому выполняется прерывание по вызову ядра.

clip_image001

Семантика RPC в случае отказов

1. Клиент не может определить местонахождения сервера, например, в случае отказа нужного сервера, или из-за того, что программа клиента была скомпилирована давно и использовала старую версию интерфейса сервера. В этом случае в ответ на запрос клиента поступает сообщение, содержащее код ошибки.

2. Потерян запрос от клиента к серверу. Самое простое решение - через определенное время повторить запрос.

3. Потеряно ответное сообщение от сервера клиенту

4. Сервер потерпел аварию после получения запроса

Существует три подхода к этой проблеме:

· Ждать до тех пор, пока сервер не перезагрузится и пытаться выполнить операцию снова. Этот подход гарантирует, что RPC был выполнен до конца по крайней мере один раз, а возможно и более.

· Сразу сообщить приложению об ошибке. Этот подход гарантирует, что RPC был выполнен не более одного раза.

· Третий подход не гарантирует ничего. Когда сервер отказывает, клиенту не оказывается никакой поддержки. RPC может быть или не выполнен вообще, или выполнен много раз. Во всяком случае этот способ очень легко реализовать.

Как поступать с сиротами? Рассмотрим 4 возможных решения

Уничтожение. До того, как клиентский стаб посылает RPC-сообщение, он делает отметку в журнале, оповещая о том, что он будет сейчас делать

Перевоплощение. В этом случае все проблемы решаются без использования записи на диск

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

Истечение срока. Каждому запросу отводится стандартный отрезок времени Т, в течение которого он должен быть выполнен.

Алгоритм прямого преобразования исходной программы в триадную форму

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

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

Номер элемента

Синтаксическая категория (класс) элемента (идентификатор, константа, число, строка и т.д.)

Информация о программных переменных, константах и т.д.

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

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

Разумеется, ими должны быть те конструкции исходного языка, которые не принимают активного участия в процессе оптимизации. К ним можно отнести комментарии, операторы DECLARE в языке ПЛ/1 и операторы описания типа DIMENSION, COMMON, EQUIVALENCE в языке Фортран.

Однако исключение какого-либо оператора исходного языка из промежуточного представления не должно приводить к потере информации об исходной программе. Для этого необходимо предусмотреть формирование таблиц, представляющих ту информацию, которая со держится в конструкциях исходного языка, не отраженных в промежуточной форме. Например, для отражения информации, содержащейся в операторе DECLARE, следует создать таблицу, содержащую программные переменные и их характеристики (атрибуты). В этой таблице необходимо также предусмотреть поле для указания областей действия переменных в программе.

Таким образом, при преобразовании исходной программы необходимо формировать таблицу INV, в которой должны быть записаны элементы, входящие в операторы, которые в промежуточной форме не представлены. Это обстоятельство должно быть отмечено в ОПЗ программы с помощью соответствующих признаков, что и позволит при обратном преобразовании программы из промежуточной формы в исходный язык использовать эту таблицу для восстановления исходной формы программы.

Операции

Приоритет

ПЛ/1

Фортран

Во входной строке

В стеке

(

(

1

1

)

)

1

Нет

ЭМ, Ф, ФАП, ФОП

ЭМ, Ф, ФАП, ФОП, ВПУ

Нет

2

,

,

3

4

|

.OR.

5

6

&

.AND.

7

8

=, >, <,!=, !<, !>, <=, >=

.EQ., .GT., .LT., .NE., .LE., .GE.

9

10

||

||

11

12

+, –

+, –

13

14

*, /

*, /

15

16

И (унарный минус), **, !

И (унарный минус), **, !

17

18

Аналогично при построении элементов исходной программы следует формировать таблицу, состоящую из элементов, являющихся именами процедур (подпрограмм).

Введем некоторые уточнения в принципы, положенные в основу алгоритмов преобразования исходных программ в промежуточную форму (ОПЗ и триады) и изложенные в предыдущем параграфе.

Прежде всего установим приоритеты всех операций, имеющихся во входных языках. В таблице приведены приоритеты всех операций языков ПЛ/1 и Фортран. Здесь не учитывается тот факт, что в языке ПЛ/1 наиболее приоритетные операции выполняются справа налево.

Ввод операций ЭМ, Ф, ФАП, ФОП, ВПУ в промежуточное представление программы производится следующим образом. Если при преобразовании исходной программы в ОПЗ элемент Е(I), рассматриваемый во входной строке исходной программы, является открывающей скобкой «(», то он записывается в стек операций. Сразу же после этого в стеке операций производится следующая запись.

- ЭМ, если предыдущий элемент Е(I—1) являет идентификатором массива;

- Ф, если предыдущий элемент Е(1—1) является идентификатором встроенной (стандартной) функции или процедуры-функции (подпрограммы-функции);

- ФАП, если предыдущий элемент Е(1—1) является именем процедуры-подпрограммы в операторе вызова;

- ФОП, если предыдущий элемент Е(1—1) является ключевым словом PROCEDURE (для языка ПЛ/1) или именем подпрограммы в операторе SUBROUTINE (для языка Фортран);

- ВПУ, если предыдущий элемент Е(1—1) являете ключевым словом IF в языке Фортран.

В противном случае, после того как открывающая скобка помещена в стек операций, следует перейти рассмотрению следующего (I+1)-го элемента входной строки.

Из таблицы видно, что операции ЭМ, Ф, ФАП, ФОП и ВПУ из стека может вытолкнуть и в ОПЗ поместит. только закрывающая скобка.

При преобразовании программы с исходного язык, в ОПЗ необходимо предварительно определить совокупность разделителей (ограничителей) из исходного языка, которые в стеке операций не обрабатываются, но прежде чем переписать их в ОПЗ, необходимо очистить стек. Этим разделителям назначается нулевой приоритет. В языках программирования такими разделителями являются, например знак конца оператора «;» ключевые слова THEN, ELSE, ТО, BY, WHILE и др.

Таким образом, прежде чем переписать из входной строки в ОПЗ элементы исходной программы, которые имеют нулевой приоритет, необходимо очистить стек операций.

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

Возможные усовершенствования триадной формы записи

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

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

Рассмотрим выражение общего вида

A=B Q1 C Q2 D, (*)

где Q1 и Q2 — знаки обобщенных арифметических операций с одинаковыми приоритетами. ОПЗ этого выражения будет иметь вид

ABC Q1 D Q2=

Заметим, что для выявления идентичности двух элементарных вычислений не имеет значения, какой вид имеет соответствующая триадная строка: mi Q a, b или mi Q b, a.

clip_image002

Если Q1 не является операцией деления или вычитания, то (*) может быть преобразовано в триадную форму тремя различными способами. В результате получим следующие триадные формы:

clip_image004

Если Q1 и Q2 — соответственно операции деления и умножения или вычитания и сложения, то формы 1) и 3) остаются без изменения, а форма 2) будет иметь вид:

clip_image006

Если Q1 и Q2 — обе операции деления или операции вычитания, то опять изменится только форма 2), которая будет иметь вид:

Форма 1) получается обычным методом получения триад из ОПЗ. Для получения из ОПЗ форм 2) и 3) необходимо придерживаться следующего простого правила. При просмотре ОПЗ, если рассматриваемый элемент и после него второй по счету элемент (обозначим его через J) являются знаками арифметической операции приоритеты этих знаков одинаковы, то для формы 2) первой выполняется операция Q2 (она представлена элементом J) над элементами (J—3) и (J—1), если операция Q1 (представленная элементом (J—2)) является знаком * или +. В случае, когда Q1 и Q2 — знаки операции/или —, то вместо операции Q2 выполняется операция clip_image008. Если же Q1 и Q2 соответственно знаки / и * или — и +, то для формы 2) первой выполняется операция clip_image010 над элементами (J—1) и (J—3). Для формы 3) первой выполняется операция Q2 над элементами (J–4) и (J–1).

Каким бы способом ни представлять в триадной форме рассматриваемое выражение (*), конечный результат будет одним и тем же — вычисление значения А. Это обстоятельство позволяет представлять арифметические выражения в триадной форме описываемом ниже способом, который дает возможность установить идентичность максимального числа выражений и (или) подвыражений, выполняющих одно и то же вычисление. Если при просмотре ОПЗ какого-нибудь арифметического выражения в нем не окажется подвыражений, содержащих соседние знаки операций с одинаковым приоритетом, то эти подвыражения и рассматриваемое выражение в целом преобразуются в триады обычным способом. В противном случае те подвыражения, в которых имеются два соседних знака операций с одинаковыми приоритетами, преобразуются в триады способами 1), 2) или 3) одно за другим. Эти формы являются различными триадными записями одного и того же выражения. Чтобы в процессе анализа учитывать это обстоятельство, при записи каждой из этих форм используются одни и те же номера строк, но они снабжаются различными признаками, например @, @@, @@@. Поэтому в триадах должно быть предусмотрено поле для этих признаков.

clip_image012

Рассмотрим этот способ триадной записи на примере фрагмента исходной программы:

Обратная польская запись этого фрагмента имеет вид:

clip_image016

После преобразования в триадную форму фрагмент будет иметь вид (здесь и далее рамкой выделены общие выражения):

clip_image019

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

clip_image021

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

Здесь может возникнуть следующий естественный вопрос. Как при преобразовании исходной программы в триадную форму учитывается то обстоятельство, что различные способы представления выражения (*) могу дать различные результаты вычисления в случаях, когда операнды существенно отличаются друг от друга по значению?

Нетрудно убедиться, что данный вопрос не имеет ничего общего с проблемой оптимизации.

Например, если в рассмотренном выше фрагменте программы переменные D и Е— очень малые числа а С—очень большое число, то при выбранном способе 2) представления выражения D*C/E в триадной форме может возникнуть ситуация переполнения. Данная форма записи этого выражения выбрана из-за того, что в программе имеется подвыражение С/Е. Однако независимо от способа представления выражения D*C/E в триадной форме все равно из-за выражения С/Е возникнет ситуация переполнения. А это должен учитывать программист при написании программы.

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

clip_image023

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

Следует также заметить, что если при учете двух знаков рассматриваемое подвыражение можно представить тремя способами, то при учете трех знаков — уже двенадцатью способами. Это предъявляет повышенные требования к объему ЗУ и увеличивает затраты времени на обработку программы.

По этим же соображениям для выявления максимального числа повторных выражений подвыражения типа В**К, где К — целое число, 2£К£7, в триадной форме следует представлять с помощью К—1 строки операций умножения.

Для анализа всех типов элементарных вычислений промежуточный язык должен обеспечивать возможность, представлять не только выражения с простыми переменными, но и индексированные переменные и их индексы а также встроенные (стандартные) функции и их аргументы.

Для обеспечения такой возможности введем операции ВЫБОР ЭЛЕМЕНТА МАССИВА (ЭМ), операндами которой являются идентификатор массива и значения индексных выражений, а также операцию ФУНКЦИЯ (Ф), операндами которой являются идентификатор функции и идентификаторы аргументов. Кроме того, запятую «,» будем считать также знаком операции. Её аргументы — идентификаторы или индексы, которые она разделяет.

clip_image025

Покажем использование этих новых операций на примере. Пусть имеется фрагмент исходной программы

clip_image029

Обратная польская запись этого фрагмента будет следующей (Здесь унарный минус для отличия от обычного минуса представлен символом «И»):

Соответствующая триадная запись будет иметь вид

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

clip_image032

Рассмотрим, как можно удовлетворить другое важное требование к промежуточной триадной форме — сохранить все конструкции исходного языка и тем самым структуру исходной программы. Так как процесс трансляции исходной программы нас не интересует, то нет никакой необходимости детализировать в промежуточной форме конструкции исходного языка. В частности, не надо представлять оператор цикла или условный оператор эквивалентными группами операторов. Другими словами, при преобразовании исходной программы в промежуточную форму все конструкции исходного языка можно оставить без каких-либо существенных изменений.

Рассмотрим следующий фрагмент исходной программы:

clip_image034

Его обратная польская запись имеет вид

clip_image036

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

Пользуясь такой формой записи, легко установить, что линейность выполнения программы нарушается в строках т3, т6, т10, т14 и т25. Кроме того используемая форма развертывания оператора цикла позволяет легко определить его параметры и тип, что имеет большое значение для сбора начальной информации и выполнения оптимизирующих преобразовании над циклами.

Триадная запись удобна для определения поведения элементов данных в каждом участке программы. В частности, анализируя триадное представление программы, можно установить в каждом операторе и (или) участке программы и во всей программе и целом множества элементов данных, которые определяются и которые используются. Однако для получения полной и достоверной начальной информации промежуточная триадная форма должна отражать не только внутренние переменные, но также вводимые и выводимые данные, фактические параметры, содержащиеся в операторах вызова процедур и процедур-функций, а также - формальные параметры процедур и процедур-функций. Для выполнения этого требования введем некоторые дополнительные операции. Для представления в промежуточной форме фактических параметров операторов вызова процедур введем операцию ФАКТИЧЕСКИЙ ПАРАМЕТР (ФАП), операндами которой являются имя вызываемой процедуры и идентификаторы передаваемых фактических параметров, а для представления . формальных параметров в операторах процедур—операцию ФОРМАЛЬНЫЙ ПАРАМЕТР (ФОП), аргументами которой служат ключевое слово PROCEDURE и идентификаторы формальных параметров.

clip_image038

Тогда например, фрагмент исходной программы

clip_image040

' в ОПЗ будет иметь вид

clip_image042

Используя описанные выше новые операции, получаем следующее триадное представление этого фрагмента:

Для разделения друг от друга операторов программ мы в промежуточной триадной форме после записи каждого очередного оператора используется разделитель «;». Например, mi _; _

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

clip_image044

Для подтверждения вышесказанного рассмотри следующий фрагмент исходной программы, составленный на языке Фортран-IV:

clip_image047

Для этого фрагмента усовершенствованная триадная форма имеет следующий вид:

Здесь для явного указания в промежуточной форме нарушений линейности выполнения программы в условные операторы, в частности в логический условный оператор, введена операция ВЫБОР ПО УСЛОВИЮ (ВПУ), операндами которой являются ключевое слово IF и последующее за ним логическое выражение. Несмотря на то что язык программирования Фортран не содержит знака конца оператора в промежуточной форме после каждого оператора предусмотрена отдельная строка для представления знака конца оператора «; » точно так же, как в языке ПЛ/1. Это значительно облегчает последующие этапы оптимизации. Введение знака «; » при преобразовании программы в ОПЗ необходимо для очистки стека операций по окончании преобразования каждого оператора.

Сравнительный анализ промежуточных языков

Среди известных промежуточных языков, применяемых при трансляции программ с языков высокого уров­ня, наиболее распространены обратная польская и триадная записи. Рассмотрим возможности ОПЗ и триадной записи по удовлетворению тех требований, которые предъявляются к промежуточному языку с точки зрения оптимизации программ.

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

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

Обратная польская запись операторов исходной программы безусловно отображает точную последовательность выполнения операций. Однако используемые при этом способы представления таких операторов исходной программы, как оператора безусловного перехода, ус­ловного оператора, оператора цикла и др., нарушающих линейность программы, существенно усложняют ОПЗ,.а, следовательно, и триадную запись с точки зрения ана­лиза логических и информационных связей в программе и собственно оптимизации. В то же время на уровне промежуточных языков, используемых в трансляторах не сохраняются все конструкции исходной программы Например, на уровне ОПЗ и триад оператор цикла представляется эквивалентной группой операторов присваивания, условного перехода и безусловного перехода (возврата к началу цикла). А оператор условного перехода заменяется оператором безусловного перехода на один метку по значению «ложь» и на другую метку по значению «истина».

clip_image002
Существующие методы получения триадной записи и; ОПЗ не могут отобразить все элементарные вычисления я подвыражения, которые должны быть подвергнуть оптимизации. В подтверждение этого утверждения рассмотрим, например, следующий фрагмент программы.

A=B*C*D–X**2; (1)

F=B+C–D+Y/Q1; (2)

P=(1-Z/Q)*(B–D+C); (3)

R=C*D; (4)

Обратная польская запись этого фрагмента имеет вид

ABC*D*X2**–=;

FBC+D–YQ1/+=;

P1ZQ/–BD–C+*=;

RCD*=;

Представим теперь этот фрагмент в виде последовательности триад обычным способом:

m1 * B, C m10 = F, m9

m2 * m1, D m11 / Z, Q

m3 ** X, 2 m12 – 1, m11

m4 – m2, m3 m13 – B, D

m5 = A, m4 m14 + m13, C

m6 + B, C m15 * m12, m14

m7 – m6, D m16 = P, m15

m8 / Y, Q1 m17 * C, D

m9 + m7, m8 m18 = R, m17

Так как в этой последовательности нет ни одной группы одинаковых триад, то при оптимизации не будет обнаружено ни одного повторного вычисления, хотя на самом деле они в программе есть. Нетрудно видеть, что данном случае триадная форма не позволяет представить элементарное вычисление C*D в операторе (1) и всевозможные варианты вычисления подвыражения B–D+C в операторе (3). Между тем если бы триадная запись позволила представить все элементарные вычисления то после оптимизации рассматриваемый фрагмент исходной программы имел бы следующий вид:

T1=C*D;

T2=B+C–D;

A=B*T1–X**2;

F=T2+Y/Q1;

P=(1–Z/Q)*T2;

R=T1;

Таким образом, триады, полученные существующими методами преобразования, не отражают всех возможны элементарных вычислений, когда в выражениях или под выражениях следуют друг за другом непосредственно два или более знака арифметических операций с одинаковыми приоритетами, такие, например, как * и / или + и –. Это оказывает существенное влияние на полноту оптимизации. Предлагавшиеся ранее методы факторизации последовательности выражений и переупорядочения операндов в лексографическом порядке мало пригодны для практического применения, так как, позволяют исправить этот недостаток лишь частично то только в достаточно простых случаях.

У триадной записи с точки зрения обнаружения программе общих вычислений и подвыражений имеет' еще один недостаток. Рассмотрим его на следующем примере. Допустим, что исходная программа содержит фрагмент

A=(B+X**4)/(C–X**3);

D=X**3–E+X**2;

обратная польская запись этого фрагмента имеетcя дующий вид:

ABX4**+CX3**–/=;

DX3**E–X2**+=;

Триадная запись этого фрагмента имеет вид

m1 ** X, 4 m7 = X, 3

m2 + B, m1 m8 / m7, E

m3 ** X, 3 m9 – X, 2

m4 – C, m3 m10 – m8, m9

m5 / m2, m4 m11 + D, m10

m6 = A, m5

Как видно из триадной записи, при просмотре этого фрагмента исходной программы в качестве повторных; вычислений могут быть обнаружены одинаковые триад m3 и m7, представляющие подвыражения Х**З в операторах (1) и (2).

Триадная форма представления программ позволила бы производить более полную оптимизацию, если для вычисления подвыражений Х**4 и Х**З в триадах выделить не одну, а соответственно три и две строки, заменив при этом операцию возведения в степень операцией умножения, т. е.

m1 * X, X m9 = A, m8

m2 * m1, X m10 * X, X

m3 * m2, X m11 * m10, X

m4 + B, m3 m12 – m11, E

m5 * X, X m13 * X, X

m6 * m5, X m14 + m12, m13

m7 – C, m6 m15 = D, m14

m8 / m4, m7

Такой способ представления программ в триадной форме позволяет при оптимизации обнаружить в качестве повторных вычислений подвыражения Х**2 в триадах m1, m5, m10, т13 и Х**З в триадах m2, m6, m11. После оптимизации рассматриваемый фрагмент исходной программы мог бы иметь следующий вид:

T1=X*X;

T2=T1*X;

A=(B+T2*X)/(C-T2);

D=T2–E+T1;

Триадная форма записи

Кроме прямой и обратной польской записи арифметических и логических выражений, существует триадная форма записи, которая очень удобна для представления в явном виде последовательности элементарных вычислений, входящих в арифметические и логические выра­жения, и, тем самым, процесса вычисления этих выражений. Триадная запись позволяет расчленить ОПЗ выражений на отдельные элементарные вычисления.

Каждое элементарное вычисление в триадной форме представляется с помощью одной строки, которая в общем случае имеет следующий вид:

mi КОП a1,a2

где КОП — код операции; a1,a2 первый и второй операнды, над которыми выполняется данная операция; mi номер строки в триадной записи или номер триады.

Если выполняемая операция не является операцией присваивания, то номер триады одновременно служит идентификатором результата вычисления.

Триадная запись для простого арифметического выражения A=B+C–D будет иметь вид

m1 + B, C

m2m1, C

m3 = A, m2

В процедуре преобразования выражений из ОПЗ в триадную форму используется стек операндов. Сущность процедуры состоит в том, что ОПЗ выражения просма­тривается слева направо и выполняются следующие

шаги:

1. Рассмотреть очередной элемент из ОПЗ выражения.

2. Если рассматриваемый элемент является операн­дом, то занести его в вершину стека операндов. Вернуться к шагу 1.

3. Сформировать очередную строку триадной записи в следующей последовательности:

3.1. Из вершины стека операндов удалить два верхних элемента и поместить их в поле операндов форми­руемой строки. При этом элемент, удаленный из стека операндов первым, поместить в поле второго операнда. Если рассматриваемый знак операции является унарным минусом, то из вершины стека вытолкнуть только один элемент, а в качестве первого операнда формируемой строки триадной записи взять нуль.

3.2. Номер сформированной триады занести в вершину стека операндов.

3.3. Выполнить возврат к шагу 1.

Процедуру преобразования ОПЗ простых выражений в триадную форму можно рассмотреть на примере уже использованного выше выражения А=(В*(С—D/K)+ E)/F (После рассмотрения элемен­тов ОПЗ они далее не указываются.)

ТЕСТИРОВАНИЕ ПУТЕМ ПОКРЫТИЯ ЛОГИКИ ПРОГРАММЫ

 

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

clip_image002

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

протестирована. Эквивалентная программа, написанная на языке PL/I, имеет вид:

М: PROCEDURE (А, В, X);

IF ((А>1) & (В=0)) THEN DO; Х=Х/А; END;

IF ((A==2) I (X>D) THEN DO:X=X+1; END;

END;

 

Можно выполнить каждый оператор, записав один-единственный тест, который реализовал бы путь- асе. Иными словами, если бы в точке а были установлены значения A=2, В=0 и Х=3, каждый оператор выполнялся бы один раз (в действительности Х может принимать любое значение). К сожалению, этот критерий хуже, чем он кажется на первый взгляд. Например, пусть первое решение записа­но как или, а не как и. При тестировании по данному критерию эта ошибка не будет обнаружена. Пусть второе ре­шение записано в программе как Х>0, эта ошибка так­же не будет обнаружена. Кроме того, существует путь, в котором Х не изменяется (путь abd). Если здесь ошибка, то и она не будет обнаружена. Таким образом, критерий покрытия операторов является настолько слабым, что его обычно не используют.

Более сильный критерий покрытия логики программы известен как покрытие решений, или покрытие переходов. Согласно данному критерию должно быть записано дос­таточное число тестов, такое, что каждое решение на этих тестах примет значение истина и ложь, по крайней мере, один раз. Иными словами, каждое направление перехода должно быть реализовано, по крайней мере, один раз. Примерами операторов перехода или решений являются операторы DO (или PERFORM UNTIL в Коболе), IF, многовыходные операторы GO ТО.

Можно показать, что покрытие решений обычно удовлетворяет критерию покрытия операторов. Посколь­ку каждый оператор лежит на некотором пути, исходящем либо из оператора перехода, либо из точки входа программы, при выполнении каждого направления перехода каждый оператор должен быть выполнен. Однако существует, по крайней мере три исключения. Первое— патологическая ситуация, когда программа не имеет ре­шений. Второе встречается в программах или подпрограммах с несколькими точками входа; данный оператор мо­жет быть выполнен только в том случае, если выполнение программы начинается с соответствующей точки вхо­да. Третье исключение—операторы внутри ON-единнц; выполнение каждого направления перехода не обязатель­но будет вызывать выполнение всех ON-единиц. Так как покрытие операторов считается необходимым условием, покрытие решений, которое представляется более сильным критерием, должно включать покрытие операторов. Следовательно, покрытие решений требует, чтобы каждое решение имело результатом значения истина и ложь, и при этом каждый оператор выполнялся бы, по крайней мере, один раз. Альтернативный и более легкий способ выражения этого требования состоит в том, чтобы каждое решение имело результатом значения истина и ложь и что каждой точке входа (включая ON-единицы) должно быть передано управление при вызове программы, по крайней мере, один раз.

Изложенное выше предполагает только двузначные решения или переходы и должно быть модифицировано для программ, содержащих многозначные решения. Примерами таких программ являются программы на PL/I, включающие операторы SELECT (CASE) или операторы GO ТО, использующие метку-переменную, программы на Фортране с арифметическими операторами IF, вычис­ляемыми операторами GO ТО или операторами GO ТО по предписанию, и программы на Коболе, содержащие операторы GO ТО вместе с ALTER или операторы GO— ТО—DEPENDING—ON. Критерием для них является вы­полнение каждого возможного результата всех решений по крайней мере один раз и передача управления при вызове программы или подпрограммы каждой точке входа по крайней мере один раз.

В программе, представленной на рис. 4.1, покрытие решений может быть выполнено двумя тестами, покрыва­ющими либо пути асе и abd, либо пути acd и abe. Если мы выбираем последнее альтернативное покрытие, то входами двух тестов являются А=3, В=0, Х=3 и А=2, В=1,Х=1.

Покрытие решений — более сильный критерий, чем по­крытие операторов, но н он имеет свои недостатки. Например, путь, где Х не изменяется (если выбрано первое альтернативное покрытие), будет проверен с вероятностью 50%. Если во втором решении существует ошибка (например, Х<1 вместо Х>1), то ошибка не будет обнаружена двумя тестами предыдущего примера.

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

DO К=0 ТО 50 WHILE (J+K<QUEST);

содержит два условия: К меньше или равно 50 и J+K меньше, чем QUEST. Следовательно, здесь требуются тесты для ситуаций J£50, K>50 (т.е. выполнение последней итерации цикла), J+K<QUEST и J+К³ QUEST.

Программа рис. 4.1 имеет четыре условия: А> 1, В=0, А=2 и Х>1. Следовательно, требуется достаточное число тестов, такое, чтобы реализовать ситуации, где А>1, А£1, В¹0 и В=О в точке а и А=2,А¹2, X>1, и Х£1 в точке b. Тесты, удовлетворяющие критерию покрытия условий, и соответствующие им пути:

1. А=2, В=0, Х=4 асе

2. А=1, .B=1, X=1 abd.

Заметим, что, хотя аналогичное число тестов для этого примера уже было создано, покрытие условий обычно лучше покрытия решений, поскольку оно может (но не всегда) вызвать выполнение решений в условиях, не реа­лизуемых при покрытии решений. Например, оператор

DO К=0 ТО 50 WHILE (J+K<QUEST);

представляет собой двузначный переход (либо выполня­ется тело цикла, либо выход из цикла). Если использо­вать тестирование решений, то достаточно выполнить цикл при изменении K от 0 до 51 без проверки случая, когда условие в WHILE ложно. Однако при критерии по­крытия условий необходим тест, который реализовал бы результат ложь условия J+ K<QUEST.

Хотя применение критерия покрытия условий на первый взгляд удовлетворяет критерию покрытия решений, это не всегда так. Если тестируется решение IF (А&В), то при критерии покрытия условий требовались бы два теста — А есть истина, В есть ложь и А есть ложь, В есть истина. Но в этом случае не выполнялось бы THEN — предложение оператора IF. Тесты критерия покрытия ус­ловий для ранее рассмотренного примера покрывают результаты всех решений, но это только случайное совпадение. Например, два альтернативных теста

clip_image005clip_image006

1. A=1, B=0, X=3

2. A=2, B=1, X=1

покрывают результаты всех условий, но только два из четырех результатов решений (они оба покрывают путь abe и, следовательно, не выполняют результат истина первого решения и результат ложь второго решения).

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

Недостатком критерия покрытия решений/условий является невозможность его применения для выполнения всех результатов всех условий; часто подобное выполнение имеет место вследствие того, что определенные условия скрыты другими условиями. В качестве примера рассмотрим приведенную на рис. 4.2 схему передач управления в машинном коде программы рис. 4.1. Многоусловные решения исходной программы здесь разбиты на отдельные решения и переходы, поскольку большинство машин не имеет команд, реализующих решения c многими исходами. Наиболее полное покрытие тестами в этом случае осуществляется таким образом, чтобы вы­полнялись все возможные результаты каждого простого решения. Два предыдущих теста критерия покрытия ре­шений не выполняют этого; они недостаточны для выполнения результата ложь решения Н и результата истина решения К. Набор тестов для критерия покрытия усло­вий такой программы также является неполным; два теста (которые случайно удовлетворяют также и крите­рию покрытия решений/условий) не вызывают выполне­ния результата ложь решения 1 и результата истина ре­шения К.

Причина этого заключается в том, что, как показано на рис. 4.2, результаты условий в выражениях и и или мо­гут скрывать и блокировать действие других условий. На­пример, если условие и есть ложь, то никакое из после­дующих условий в выражении не будет выполнено. Ана­логично если условие или есть истина, то никакое из по­следующих условий не будет выполнено. Следовательно, критерии покрытия условий и покрытия решений/условий недостаточно чувствительны к ошибкам в логических вы­ражениях.

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

NOTFOUND=’1’В;

DO I=1 ТО TABSIZE WHILE (NOTFOUND); /* поиск в таблице */

последовательность операторов, реализующая процедуру поиска,

END;

существуют четыре ситуации, которые должны быть протестированы: .

1. I £ TABSIZE и NOTFOUND есть истина.

2. I £ TABSIZE и NOTFOUND есть ложь (обнаружение необходимого искомого значения до достижения кон­ца таблицы).

3. I > TABSIZE и NOTFOUND есть истина (достижение конца таблицы без обнаружения искомого значения).

4. I >TABSIZE и NOTFOUND есть ложь (искомое значение является последней записью в таблице).

Легко видеть, что набор тестов, удовлетворяющий критерию комбинаторного покрытия условий, удовлетво­ряет также и критериям покрытия решений, покрытия условий и покрытия решений/условий.

По этому критерию в программе рис. 4.1 должны быть покрыты тестами следующие восемь комбинаций:

1.A > 1, В = 0 5 = 2, Х > 1

2.A > 1. B ¹ 0 6.A = 2, X £ l

3.A £ 1 ,В = 0 7.A ¹ 2, X > 1

4.A £ 1, В ¹ 0 8.A ¹ 2, X £ I

Заметим, что комбинации 5—8 представляют собой значения второго оператора IF. Поскольку Х может быть изменено до выполнения этого оператора, значе­ния, необходимые для его проверки, следует восстано­вить исходя из логики программы с тем, чтобы найти со­ответствующие входные значения.

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

A=2, В=0, Х=4 покрывает 1,5

А=2, В=1=1 покрывает 2,6

А=1, В=0, Х=2 покрывает 3,7

А=1, В=1, Х=1 покрывает 4,8

То, что четырем тестам соответствуют четыре различ­ных пути на рис. 4.1, является случайным совпадением. На самом деле представленные выше тесты не покрывают всех путей, они пропускают путь acd. Например, требует­ся восемь тестов для тестирования следующей про­граммы:

IF((X=Y) & (LENGTH(Z)=0) &END)THEN J-l; ELSE I=l;

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

Таким образом, для программ, содержащих только одно условие на каждое решение, минимальным являет­ся критерий, набор тестов которого 1) вызывает выпол­нение всех результатов каждого решения по крайней ме­ре один раз и 2) передает управление каждой точке вхо­да (например, точке входа, ON-единице) по крайней мере один раз (чтобы обеспечить выполнение каждого опера­тора программы по крайней мере один раз). Для про­грамм, содержащих решения, каждое из которых имеет более одного условия, минимальный критерий состоит из набора тестов, вызывающих выполнение всех возможных комбинаций результатов условий в каждом решении и передающих управление каждой точке входа программы по крайней мере один раз. (Слово «возможных» упот­реблено здесь потому, что некоторые комбинации условий могут быть нереализуемыми; например, в выражении (А>2)&(A<10) могут быть реализованы только три комбинации условий.)

Методы тестирования программ

Результаты психологических исследований, обсуж­давшиеся в гл. 2, показывают, что наибольшее внимание при тестировании программ уделяется проектированию или созданию эффективных тестов. Это связано с невозможностью «полного» тестирования программы, т. е. тест для любой программы будет обязательно неполным (иными словами, тестирование не может гарантировать отсутствия всех ошибок). Стратегия проектирования заключается в том, чтобы попытаться уменьшить эту «не­полноту» на столько, на сколько это возможно.

Если ввести ограничения на время, стоимость, ма­шинное время и т. п то ключевым вопросом тестирова­ния становится следующий:

Какое подмножество всех возможных тестов име­ет наивысшую вероятность обнаружения большин­ства ошибок?

Изучение методологий проектирования тестов дает ответ на этот вопрос.

По-видимому, наихудшей из всех методологий явля­ется тестирование со случайными входными значения­ми (стохастическое) — процесс тестирования программы путем случайного выбора некоторого подмножества из всех возможных входных величин. В терминах вероят­ности обнаружения большинства ошибок случайно вы­бранный набор тестов имеет малую вероятность быть оптимальным или близким к оптимальному подмноже­ством. В настоящей главе рассматривается несколько подходов, которые позволяют более разумно выбирать тестовые данные. В гл. 2 было показано, что исчерпыва­ющее тестирование по принципу черного или белого ящика в общем случае невозможно. Однако при этом отмечалось, что приемлемая стратегия тестирования может обладать элементами обоих подходов. Таковой является стратегия, излагаемая в этой главе. Можно раз­работать довольно полный тест, используя определен­ную методологию проектирования, основанную на прин­ципе черного ящика,' а затем дополнить его проверкой логики программы (т. е. с привлечением методов белого ящика).

Методологии тестирования представлены ниже.

Черный ящик

Белый ящик

Эквивалентное разбиение

Анализ граничных значений

Применение функциональных диаграмм

Предположение об ошибке

Покрытие операторов

Покрытие решений

Покрытие условий

Покрытие решений/условий

Комбинаторное покрытие условий

Хотя перечисленные методы будут рассматриваться здесь по отдельности, при проектировании эффективного теста программы рекомендуется использовать если не все эти методы, то, по крайней мере, большинство из них, так как каждый метод имеет определенные достоинства и недостатки (например, возможность обнаруживать и про­пускать различные типы ошибок). Правда, эти методы весьма трудоемки, поэтому некоторые специалисты, ознакомившись с ними, могут не согласиться с данной рекомендацией. Однако следует представлять себе, что тестирование программы—чрезвычайно сложная задача. Для иллюстрации этого приведем известное изрече­ние: «Если вы думаете, что разработка и кодирование программы — вещь трудная, то вы еще ничего не видели».

Рекомендуемая процедура заключается в том, чтобы разрабатывать тесты, используя методы черного ящика, а затем как необходимое условие—дополнительные тес­ты, используя методы белого ящика. Методы белого ящи­ка, получившие более широкое распространение, обсуж­даются первыми.