Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
kernigan_paik.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.91 Mб
Скачать

7.2. Замеры времени и профилирование

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

% time slowprogram

real 7.0

user 6.2

sys 0.1

%

Эта команда запускает программу и возвращает три числа, означающих время в секундах: время real — физическое время, израсходованное до за­вершения работы программы; время user — время процессора, потраченное на исполнение самой программы; время sys — время, потраченное на программу операционной системой. Если в вашей системе есть аналогичная команда, используйте ее — числа будут более информативными и надежными, и делать отсчеты будет проще, чем при использовании секундомера. Ведите подробные записи. При работе над программой — внесении модификаций и/или проведении измерений — у вас накопится огромное количество данных, в которых вам будет трудно разобраться по памяти уже через день-два. (Какая именно версия работает на 20 % быстрее?) Многие технологии, которые мы обсуждали в главе о тестировании, могут быть адаптированы и к измерениям времени и улучшению производитель­ности. Используйте компьютер для запуска набора тестов и измерения времени их работы, а самое главное — используйте регрессивное тестиро­вание, чтобы удостовериться в том, что "улучшения" производительности не нарушили корректности программы.

Если в вашей системе нет команды time или вы хотите замерить время работы отдельно взятой функции, не так трудно создать себе оснастку для таких замеров — по аналогии с тестовой оснасткой. В С и C++ су­ществует стандартная функция clock, которая сообщает, сколько про­цессорного времени программа использовала до текущего момента. Ее можно вызвать перед интересующей вас функцией и после нее и таким образом вычислить время ее исполнения процессором:

#include <time.h>

#include <stdio.h>

…….

clock_t before;

double elapsed;

before = clock();

long_running_functlon();

elapsed = clock() - before;

printf("Функция использовала %.3f секунд\n",

elapsed/CLOCKS_PER_SEC);

Масштабирующий коэффициент CLOCKS_PER_SEC характеризует разре­шение таймера, возвращаемое clock. Если функция выполняется за дол и секунды, запустите ее в цикле, но (если измерения должны быть особо точными) не забудьте компенсировать затраты времени на сам цикл:

before = clock();

for (i = 0; i < 1000; i++)

short_running_function();

elapsed = (clock()-before)/(double)i;

В Java функции класса Date выдают текущее системное время, которое приблизительно равно времени, использованному процессором:

Date before = new Date();

long_running_function();

Date after = new Date();

long elapsed = after. getTimeO - before. getTime();

Значение, возвращаемое getTime, измеряется в миллисекундах.

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

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

В 1971 году в работе, где впервые был представлен термин "профили­рование", Дон Кнут написал, что, "как правило, менее 4 % программы поглощают более половины от всего времени исполнения". Принимая во внимание это замечание, можно сделать вывод о способах применения профилирования: с его помощью определяются участки программы, по­глощающие большую часть времени работы, в них вносятся улучшения, и процесс повторяется еще раз для поиска новых критических мест. Вы­ясняется, что после одной-двух подобных итераций характерных "горя­чих точек" не остается.

Профилирование обычно включается специальным флагом или оп­цией компилятора. Программа выполняется, и после этого показывают­ся результаты работы профилировщика. В Unix таким флагом является, как правило, -р, а профилировщик называется prof:

% ее -р spamtest.c -о spamtest

% spamtest

% prof spamtest

В приводимой далее таблице показан профиль, сгенерированный рабочей версией спам-фильтра, которую мы создали специально для того, чтобы разобраться в его поведении. В ней используется фиксированное сообще­ние и фиксированный набор из 217 фраз, который сравнивается с сообще­нием 10 000 раз. Запуск производился на машине MIPS R10000 250 MHz; использовалась изначальная версия strstr — с вызовами других стандарт­ных функций. Результаты были переформатированы для более удачного размещения на странице. Обратите внимание на то, что размер ввода (217 фраз) и количество запусков (10 000) можно использовать для проверки правильности работы — эти числа появляются в колонке "calls" (вызовы), в которой показано количество вызовов каждой функции.

12234768552: Total number of instructions executed

13961810001: Total computed cycles

55.847: Total computed execution time (sees.)

1.141: Average cycles / instruction

sees % cum% cycles instructions calls function

45.260 81.0 81.0 11314990000 9440110000 48350000 strchi

6.081 10.9 91.9 1520280000 1566460000 46180000 strncmp

2.592 4.6 96.6 648 080 000 854 500 000 2 170 000 strstr

1.825 3.3 99.8 456225559 344882213 2170435 strlen

0.088 0.2 100.0 21950000 28510000 10000 isspam

0.000 0.0 100.0 100025 100028 1 main

0.000 0.0 100.0 53677 70268 219 _memccpy

0.000 0.0 100.0 48888 46403 217 strcpy

0.000 0.0 100.0 17989 19894 219 fgets

0.000 0.0 100.0 16798 17547 230 _malloe

0.000 0.0 100.0 10305 10900 204 realfree

0.000 0.0 100.0 6293 7161 217 estrdup

0.000 0.0 100.0 6032 8575 231 cleanirec

0.000 0.0 100.0 5932 5729 1 readpat

0.000 0.0 100.0 5899 6339 219 getline

0.000 0.0 100.0 5500 5720 220 _malloc

Очевидно, что в затратах времени доминируют две функции: strchr и strncmp, причем обе вызываются из strstr. Итак, Кнут ориентирусч нас правильно: небольшая часть программы поглощает большую часть времени ее исполнения. При первом профилировании программы очень часто выясняется, что лидирующие в профиле две-три функции поглоща­ют половину времени и более, как в приведенном случае, и, соответствен но, становится сразу.же ясно, на чем сконцентрировать внимание.

Концентрируйтесь на критических местах. Переписав strstr, мы еще раз выполнили профилирование для spamtest и выяснили, что теперь 99.8 % времени тратит собственно strstr, хотя в целом программа и стала рабо­тать быстрее. Когда одна функция является таким явным узким местом программы, существует только два способа исправить положение: улучшить саму функцию, применив более эффективный алгоритм, или же из­бавиться от этой функции вообще, переписав всю программу.

Мы выбрали второй способ — переписали программу. Ниже приве­дены первые несколько строк профиля работы программы spamtest, использующей последнюю, наиболее быструю реализацию isspam. При этом общее время работы программы существенно уменьшилось, критической точкой стала функция memcmp, a isspam стала занимать более существенную долю от общего времени. Эта версия гораздо сложнее предыдущей, однако эта сложность окупается с лихвой бла­годаря отказу от использования strlen и strchr в isspam и замене strncmp на memcmp, которая тратит меньше усилий на обработку каж­дого байта.

sees % cum% cycles instructions calls function

3.524 56.9 56.9 880 890 000 1 027 590 000 46 180 000 memcmp

2.662 43.0 100.0 665550000 902920000 10000 isspam

0.001 0.0 100.0 140304 106043 652 strlen

0.000 0.0 100.0 100025 100028 1 main

Весьма поучительно будет потратить некоторое время на сравнение счетчиков циклов и количества вызовов различных функций в двух про­филях. Отметьте, что количество вызовов strlen упало от нескольких миллионов до 652, a strncmp и memcmp вызываются одинаковое количе­ство раз. Обратите внимание и на то, что isspam, которая сейчас взяла на себя и функцию strchr, умудряется обойтись гораздо меньшим количе­ством циклов благодаря тому, что она проверяет на каждом шаге только нужные образцы. Изучение чисел может раскрыть и многие другие дета­ли хода выполнения программы.

"Горячая точка" нередко может быть уничтожена или, по крайней мере, существенно "охлаждена" гораздо более простыми способами, чем мы применили для спам-фильтра. Когда-то давным-давно профилиро­вание Awk показало, что одна функция вызывается примерно миллион раз — в таком цикле:

? for (j = i; j < MAXFLO; j++)

? clear(j);

Выполнение этого цикла, в котором поля очищались перед считыванием каждой новой строки ввода, занимало почти 50 % от всего времени ис­полнения. Константа MAXFLD — максимальное количество полей в строке ввода — была равна 200. Однако в большинстве случаев использования Awk реальное количество полей не превышало двух-трех. Таким обра­зом, огромное количество времени тратилось на очистку полей, которые никогда и не устанавливались. Замена константы на предыдущее значе­ние максимального количества полей дало выигрыш в скорости 25 %. Все исправления свелись к изменению верхней границы цикла:

for (j = i; j < maxfld; j++)

clear(j);

maxfld = i;

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

На представленном ниже графике показано влияние размера массива хэш-таблицы на время исполнения С-версии программы ma rkov; в качестве вводимого текста использовались Псалмы (42 685 слов, 22 482 префикса). Мы поставили два эксперимента. В первой серии запусков программа ис­пользовала размеры массивов, которые являлись степенями двойки — от 2 до 16 384; в другой версии использовались размеры, которые являлись мак­симальным простым числом, меньшим каждой степени двойки. Мы хотели посмотреть, приведет ли использование простых чисел в качестве размеров массивов к ощутимой разнице в производительности.

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

Упражнение 7-2

Вне зависимости от того, есть на вашей машине команда time или нет, используйте clock или getTime для создания собственного измерителя времени. Сравните его результаты с системным временем. Как влияет на результаты другая активность на машине?

Упражнение 7-3

В первом профиле strchr вызывалась 48 350 000 раз, a strncmp — толь­ко 46 180 000 раз. Объясните это различие.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]