LXF121:Профилирование

Материал из Linuxformat.

Перейти к: навигация, поиск

Содержание

Оценка быстродействия и профилирование

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

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

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

Повторяющийся процесс

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

Важно убедиться, что ваши числа состоятельны (то есть вы понимаете, что получили) и сосредоточить усилия на том участке, где эффект будет максимален. Явно не стоит двое суток биться над алгоритмом, если задержка обусловлена записью на диск.

Начальная оценка

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

#!/usr/bin/perl -w
 use strict;
 use Benchmark qw (:timethis);
 my $count = 10;
 timethis($count, sub {`/путь/к/программе`});

Он написан на Perl, но поскольку для вызова тестируемого кода используются обратные апострофы (`), его можно использовать для хронометража программы на любом языке, запускаемой из командной строки. В тексте скрипта вы заметите $count – это количество итераций. Другой формат – знак минус и минимальное число секунд времени процессора на прогон: например, ввод -5 приведет к запуску как минимум на 5 секунд процессорного времени.

Benchmark.pm, использованный выше модуль Perl, удобен также и для тестирования производительности программ не на Perl: просто применяйте апострофы для вызова кода из Perl-скрипта, как было сделано выше. Benchmark.pm обычно поставляется с достаточно свежей версией Perl, но может быть установлен и через CPAN. Подправив код, снова воспользуйтесь Benchmark.pm, чтобы выяснить, намного ли код стал быстрее:

#!/usr/bin/perl -w
 use strict;
 use Benchmark qw (:all);
 cmpthese( 10, {
               a => sub {`/путь/к/старомукоду`},
               a => sub {`/путь/к/новомукоду`},
 })

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

use Benchmark qw (:hireswallclock);

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

Скорая помощь

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

Ведем записи

Обертки выводят все полученные значения времени на консоль, но вывод можно перенаправить в файл (используйте wrapPer.pl >> benchmark.txt, чтобы результаты каждого прогона добавлялись к нему, не перезаписывая старых), для отслеживания прогресса.

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

Итак, настал момент запуска первого набора простых тестов. Помните, что во время тестирования не следует запускать другие задачи, иначе результаты собьют вас с толку. По возможности следует также выполнить скрипт-обертку, сам дающий усредненные результаты, несколько раз, и усреднить еще и их, добиваясь более показательного значения. Убедитесь, что ничего не загружаете: это может сказаться на скорости обработки или ввода/вывода. Желательно выполнить столько прогонов, на сколько у вас хватит времени или терпения: чем больше прогонов, тем точнее будет результат.

Профилирование: расчленяем код

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

  • раздел работы с диском (например, чтение или запись);
  • создание и/или заполнение структур данных;
  • алгоритмы и вычисления;
  • какая-либо работа с сетью (но, вероятно, здесь ускорение будет вне вашего контроля).

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

Для примера допустим, что имеется три основных раздела кода: чтение данных, проведение вычислений и запись новых данных. Создайте три версии вашего кода:

A: Полная версия
B: Только чтение данных (без расчетов и вывода).
C: Чтение данных и вычисления (без вывода).

Пусть тестирование трех этих версий дало следующее: версия А выполняется 4 секунды, B – 1 секунду и C – 3,5 секунды. То есть чтение данных занимает 1 секунду (из B). Вывод данных занимает 0,5 секунды (разность между А и С). И, следовательно, вычисления идут 2,5 секунды. Вы можете найти это по формуле A – B – вывод данных = время работы (в нашем случае 4 – 1 – 0,5 = 2,5). Вот время, требуемое для работы без ввода и вывода данных.

Вы можете использовать данный метод при хронометраже других участков кода, которые трудно отделить.

Альтернатива – выводить в контрольных точках системное время. Скрипт Perl, приведенный ниже, cгенерирует при запуске по одной строке на прогон кода:

#!/usr/bin/perl -w
 use strict;
 use Time::HiRes;
 sub time_print;
 print time_print . “:”;
 # здесь код
 print time_print . “:”;
 # еще код
 print time_print . “\n”;
 
 sub printtime {
   my ($t1,$t2) = Time::HiRes::gettimeofday;
   my $time = “$t1.” . sprintf(“%05d”,$t2/10);
   return$time”;

и выдаст нечто подобное:

1240905933.05204:1240905934.05249:1240905935.05264:1240905936.05312

Значения в этих парах разделяются точками, а пары отделены двоеточием. Значения в паре – в секундах, прошедших с начала эпохи, и в микросекундах; первое – время запуска скрипта, а второе – время завершения. Использование модуля Time::HiRes позволяет выполнять подсчет в микросекундах, а не в целых секундах.

Вычисление времени

Альтернативой для хронометража при использовании Perl и Benchmark является вычисление разности времен при помощи

my $t1 = new Benchmark;
 # code
 my $t2 = new Benchmark;
 my $td = timediff($t1, $t2);
 print “Первый раздел потребовал $td \n”;

Однако такой результат труднее обрабатывать автоматически.

Обработка данных

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

Запуск обработчика вывода для выполнения базового профилирования участков кода

#!/usr/bin/perl -w
 use strict;
 
 my $datafile = “testout.txt”;
 my @timearray;
 my $count = 0;
 
 open DATA, $datafile;
 while (<DATA>){
   my @time = split /:/;
   push @{ $timearray[$count] }, @time;
   $count++;
 }
 close DATA;
 
 my @result;
 for my $rowref ( @timearray ) {
   my @row = @$rowref;
   for my $i ( 0 .. ($#row – 1) ) {
   $result[$i] += $row[$i+1]$row[$i];
  }
 }
 
 for my $i ( 0 .. $#result ) {
 print “Section “ . ($i+1) . “ average = “ . $result[$i] / $count . “\n”;
 }

Это приведет к выводу результата (как показано на экранном снимке), но учтите, что скрипт-тест выполнялся три раза, а этого мало для правильного профилирования.

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

Решение проблем

Как указывалось выше, важно найти в вашем коде узкое место, чтобы сосредоточить усилия там, где от этого будет больше всего пользы. Не исключено, что вы распознаете проблемный кусок кода, просто поглядев на цифры; а если не получится, постройте графики с помощью OOo, KChart, Guppi или Graphviz. Кто предпочитает работать в консоли, может взять gnuplot.

Если ваш код полон операций ввода/вывода, весьма вероятно, что они-то и есть узкое место. Скорость можно увеличить при помощи следующих шагов:

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

Для просмотра текущих параметров диска можно применить hdparm (например, hdparm -v /dev/hda). Можно увеличить скорость работы накопителя лобовой атакой – включить прямой доступ к памяти (Direct Memory Access, DMA) (hdparm -d1 /dev/hda); затем вновь оцените быстродействие и посмотрите, сработало ли это. Также можно изменить значение поддержки ввода/вывода (I/O support) опцией -c3, это также способно слегка улучшить быстродействие. Поэкспериментируйте с другими значениями hdparm, но будьте осторожны: некоторые из них могут быть опасны. Перед выполнением действий почитайте man-страницу. При желании сохранить произведенные изменения, выполните hdparm -k /dev/hda.

Скорая помощь

Опция relatime – это улучшенная версия noatime; если вы используете Ubuntu, не мешает ее попробовать.

Подгонка файловой системы

Вы можете ускорить доступ к файлам, отключив ведение временных отметок при каждом обращении к ним – то есть не делая запись в каталог при каждом открытии файла. Для этого отредактируйте /etc/fstab, добавив noatime к списку опций в четвертом столбце для каждой ФС, которую вы хотите изменить, а потом все перемонтируйте командой mount -a. Однако отсутствие записей о доступе к файлу способно вызвать проблемы в некоторых программах: почтовых или резервного копирования. Хотя можно создать для своего приложения отдельный раздел и использовать noatime только в нем.

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

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

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

Параллельная обработка

Если алгоритм больше уже не улучшается, попробуйте распараллелить обработку. Возможно, для этого придется изменить подход к написанию кода, разветвив свои действия на несколько параллельных потоков. Например, при выполнении анализа объемного набора данных, части которого не взаимодействуют друг с другом, попытайтесь запустить один и тот же алгоритм сразу на нескольких машинах, с разными частями данных. В данной статье нет места для детального рассмотрения, но начать можно с учебников, имеющихся в сети. Увы, не все задачи распараллеливаются!

Strace и ltrace

Strace выводит список системных вызовов, выполненных программой. Обычно для тестирования это перебор, но тут есть пара полезных опций. Например, -c записывает время каждого системного вызова, -r выводит относительное время каждого системного вызова, а -t – абсолютное (для включения микросекунд используйте -tt). -T выводит время, потраченное на системный вызов. С опцией -e будут отслеживаться только определенные наборы системных вызовов.

ltrace выполняет то же самое, но для библиотечных вызовов. И вновь -c посчитает время вызовов и возвратит сводку, а -t и -tt покажут время суток при запуске каждой строки.

Заключение

Для получения более подробной информации о том, где тормозит ваш Perl-код, можно запустить Devel::NYTProf или его предшественника Devel::DProf – это всеобъемлющие профилировщики с хорошей документацией. Используя данные инструменты, вы сможете найти проблемные места конкретных модулей и обдумать пути их исправления. Не забудьте потом опять выполнить профилирование, чтобы проверить, есть ли сдвиг.

Если вы используете Perl или другой скриптовый язык, и способов ускорения кода не обнаруживается, а быстродействие совершено неприемлемо, то, возможно, надо перейти на компилируемый язык – типа C, C++ или Java. Однако, прежде чем погрузиться в это и полностью переделать код, следует хорошенько убедиться, что это действительно стоит вашего времени и затраты усилий. Еще раз рассмотрите результаты тестирования и убедитесь, что сделали все возможное для ускорения и дальше здесь ехать некуда.

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

На данном этапе надо снова протестировать производительность! Выясните, сколько времени вы сэкономили (не позабыв отметить, сколько рабочего времени ушло на поправки) и нельзя ли выиграть еще. Если нужно избавиться еще от пары секунд, посмотрите, может ли помочь еще что-то? Или необходимо переделывать весь код заново?

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

Личные инструменты
  • Купить электронную версию
  • Подписаться на бумажную версию