Числа, приведенные в табл. 8.1, являются суммой пользовательского и системного времени процессора и вычислены с помощью функции
time
.
Программа
(3,3)
100*fib(1000)
hoc
5.5
5.0
bas
1.3
0.7
bc
39.7
14.9
c
<0.1
0.1
Таблица 8.1: Время
работы на PDP-11/70 (в секундах)
Можно также приспособить Си программу для определения количества времени, используемого каждой функцией. Программу нужно перетранслировать в режиме профилирования, введя флаг
– p
в каждой единице трансляции Си и при режиме загрузки. Если изменить файл
makefile
для чтения:
hoc6: $(OBJS)
сс $(CFLAGS) $(OBJS) -lm -о hoc6
чтобы команда
сс
задействовала переменную
CFLAGS
, а затем задать
$ make clean; make CFLAGS=-p
то результирующая программа будет выполняться с профилированием. После выполнения программы остается файл
mon.out
, который интерпретируется программой профилировщиком
prof
.
Для иллюстрации изложенного мы протестировали
hoc6
на приведенной выше программе Фибоначчи:
$ hoc6 <fibtest
Запуск теста
$ prof hoc6 | sed 15q
Анализ
name %time cumsec #call ms/call
_pop 15.6 0.85 32182 0.03
_push 14.3 1.63 32182 0.02
mcount 11.3 2.25
csv 10.1 2.80
cret 8.8 3.28
_assign 8.2 3.73 5050 0.09
_eval 8.2 4.18 8218 0.05
_execute 6.0 4.51 3567 0.09
_varpush 5.9 4.83 13268 0.02
_lt 2.7 4.98 1783 0.08
_constpu 2.0 5.09 497 0.22
_add 1.7 5.18 1683 0.05
_getarg 1.5 5.26 1683 0.05
_yyparse 0.6 5.30 3 11.11
$
Результаты, полученные с помощью профилировщика, также подвержены случайным вариациям, как и те, что получены с помощью функции
time
, поэтому их следует считать лишь указанием настоящих значений, а не принимать за абсолютную истину. Тем не менее при необходимости приведенные значения могут помочь повысить быстродействие программы
hoc
. Приблизительно третья часть времени тратится на запись и чтение из стека. Накладные расходы еще более возрастут, если мы будем учитывать время выполнения функций связи
csv
и
cret
между программами Си (функция
mcount
представляет собой часть программы с профилированием, полученную с помощью команды ее
– р
.). Замена вызовов функций на макрообращения даст заметную разницу во времени выполнения.
Для проверки этого предположения мы изменили
code.c
, заменив вызовы
push
и
pop
на макрокоманды,
управляющие стеком:
#define push(d) *stackp++ = (d)
#define popm *--stackp = pop /* функция все-таки нужна */
(Функция
pop
все-таки нужна в качестве кода операции нашей машины, поэтому нельзя заменить все обращения к ней.) Новая версия выполняется на 35% быстрее; время в табл. 8.1 сокращается от 5.5 до 3.7 с и от 5.0 до 3.1 с.
Упражнение 8.22
В макрокомандах
push
и
popm
не предусмотрен контроль ошибок. Прокомментируйте разумность такого решения. Как бы вы обеспечили контроль ошибок, производимый в версии с функциями, не снижая быстродействия макрокоманд?
8.8 Заключение
Ознакомившись с материалом этой главы, мы можем сделать важные выводы. Во-первых, средства для развития языков очень нужны, так как позволяют сконцентрировать внимание на интересной работе проектировании языка (с ним легко экспериментировать). Грамматика является организующей структурой при реализации: программы привязываются к грамматике и вызываются в подходящий момент в процессе разбора.
Во-вторых, и это уже философский аспект, ценна сама постановка задачи речь идет о разработке языка, а не просто о написании программы. Построение программы как языкового процессора обеспечивает регулярность синтаксиса (т.е. взаимодействие с пользователем), делает более структурированной реализацию. Кроме того, мы получаем гарантию, что новые средства будут хорошо согласовываться с уже реализованными. Под "языками", конечно, следует понимать не только традиционные языки программирования, но и уже упоминавшиеся выше в примерах языки
eqn
и
pick
, а также
yacc
,
lex
, и
make
.
Рассмотрены здесь и вопросы использования программных средств. В частности, показана роль программы
make
, которая предотвращает целый класс ошибок (например, вы забыли перетранслировать какую-то подпрограмму). Она позволяет избавиться от лишней работы и предоставляет удобный способ сгруппировать в одном файле большое число связанных и, возможно, зависимых операций.
С помощью файлов макроопределений вы можете координировать описания данных, доступных более чем в одном файле. Проводя централизацию информации, они исключают ошибки, вызванные несогласованностью применяемых версий, особенно если действуют совместно с программой
make
. Весьма важно разбить данные и подпрограммы на файлы таким образом, чтобы они не были видимы, если в этом нет необходимости.
Хотелось бы отметить, что из-за ограниченного объема книги мы мало внимания уделили тем средствам UNIX, которые применяются при разработке семейства программ
hoc
. Каждая версия программы находится в отдельном каталоге, для идентичных файлов используются связи; постоянно вызываются команды
ls
и
du
, чтобы следить за тем, какие файлы где находятся. На многие вопросы ответы дают сами программы. Например, на вопрос: "Где описана данная переменная?" отвечает программа
grep
. "Как мы внесли изменения в данную версию?" отвечает
idiff
. "Насколько велик файл?" отвечает
wc
. Пора делать копию файла обратитесь к команде
cp
. Нужно скопировать только те файлы, которые изменились со времени последнего копирования? Вам поможет в этом деле программа