- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
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