Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Обработка больших объемов данных в биоинформатике

Глава 12 из книги "Производительность приложений с открытым исходным кодом".

Оригинал: Working with Big Data in Bioinformatics
Авторы: Eric McDonald, C. Titus Brown
Перевод: А.Панин

Профилирование и измерения

Простое чтение исходного кода с учетом вопросов производительности позволило обнаружить множество фрагментов кода, требующих доработки. Однако, мы хотели систематизировано учитывать время, затрачиваемое на выполнение различных фрагментов кода. Для этой цели мы использовали несколько профайлеров: GNU Profiler (gprof) и Tuning and Analysis Utilities (TAU). Мы также реализовали инструменты непосредственно на уровне исходного кода, которые позволяли ознакомиться с подробным представлением наиболее важных параметров производительности.

Обзор кода

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

Ручная трассировка путей исполнения незнакомого кода является хорошей идеей. (Одни из авторов, Eric McDonald, не имел опыта разработки программного обеспечения из состава khmer в момент присоединения к проекту и он занимался именно этим.) Хотя утверждение о том, что профайлеры (и другие инструменты) могут генерировать графы вызовов и является правдивым, эти графы вызовов являются только абстрактными результатами. На самом деле, обход путей кода в поисках вызовов функций является гораздо более основательной и поучительной работой. Отладчики могут использоваться для таких обходов, но они не годятся для исследования путей кода, которые редко используются. Также, пошаговое передвижение по пути исполнения кода может оказаться достаточно утомительным занятием. Точки останова могут использоваться для тестирования того, происходит ли достижение определенных точек в ходе обычного исполнения кода, но их установка требует некоторого предварительного исследования и понимания кода. В качестве альтернативы может быть предложен достаточно хорошо зарекомендовавший себя вариант использования редактора с множеством панелей. Четыре панели для вывода данных обычно позволяют одновременно захватывать всю необходимую разработчику информацию, которую он может воспринимать и принимать во внимание в любой момент времени.

В ходе обзора кода был установлен ряд проблем, некоторые из которых, но не все, были впоследствии подтверждены с помощью инструментов профилирования. Некоторыми из выявленных нами проблем были:
  • Ожидание нами обработки максимальных объемов данных логикой для подсчета k-меров.
  • Излишние вызовы функции toupper, присутствующие в фрагментах кода, занятых обработкой наибольших объемов данных.
  • Ввод геномных чтений выполнялся построчно и по требованию без какой-либо оптимизации предварительного чтения.
  • Копирование на основе значений структур геномных чтений выполнялось для каждого разобранного и корректного геномного чтения.

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

Инструменты

Инструменты профилирования в первую очередь позволяют выявить время, затрачиваемое на исполнение любой определенной секции кода. Для определения длительности промежутка времени они внедряют код для измерения времени в код исследуемого приложения в процессе компиляции. Внедрение этого кода для измерения времени изменяет размер функций, что может повлиять на использование inline-функций в ходе оптимизации кода. Код для измерения времени также непосредственно увеличивает общее время исполнения кода приложения; в частности, профилирование фрагментов кода, предназначенных для обработки больших объемов данных, может привести к значительному увеличению времени его исполнения. Таким образом, если вы также измеряете все время, затрачиваемое на исполнение вашего кода, вам следует учитывать влияние системы профилирования на него. Для точной оценки этого времени может использоваться простой внешний механизм сбора данных, такой, как /usr/bin/time, с целью сравнения времени исполнения непрофилируемого и профилируемого приложения, собранного с идентичным набором флагов оптимизации и рабочих параметров.

Мы точно оценили эффект профилирования путем вычисления разности времен исполнения профилируемого и непрофилируемого кода при работе с диапазоном из k размеров - меньшие значения параметра k приводят к появлению большего количества k-меров в геномном чтении, что в свою очередь ведет к возрастанию влияния эффектов профилирования. При использовании значения k = 20 мы установили, что непрофилируемый код исполняется примерно на 19% быстрее профилируемого кода, а при использовании значения k = 30 мы также установили, что непрофилируемый код исполняется примерно на 14% быстрее профилируемого кода.

Перед выполнением оптимизаций производительности благодаря исследованию данных профилирования мы установили, что логика подсчета k-меров обрабатывала наибольшие объемы данных, о чем мы также заявили в ходе предварительного обзора кода. Немного удивляло разве что то, насколько значительные объемы данных обрабатываются с помощью этой логики (около 83% всего времени работы приложения тратилось именно на это) в отличие от операций ввода/вывода для взаимодействия с хранилищем данных (около 5% всего времени для средних параметров при низкой активности хранилища). Учитывая то, что наши экспериментальные наборы данных имели объем около 500МБ и 5ГБ, мы не ожидали таких значительных эффектов кэширования.1 К тому же, после установления контроля над методами кэширования, мы обнаружили, что они в общей сложности не добавляют более нескольких секунд времени что не превышает погрешности измерения параметров общего времени исполнения. Это обстоятельство привело нас к пониманию того, что операции ввода/вывода не были нашим главной проблемой на данном этапе процесса оптимизации кода.

Как только мы начали добавлять возможности параллельной работы программного обеспечения из состава khmer, мы разработали некоторые вспомогательные программы, использующие OpenMP [4], предназначенные для тестирования параллельного исполнения задач различными компонентами. Хотя профайлер gprof и хорошо справляется с профилированием однопоточных приложений, он не предоставляет возможности проводить трассировку на уровне исполняемых потоков в процессе использования множества потоков, при этом он не работает с такими системами параллельного исполнения, как OpenMP. В приложениях, разработанных с использованием языков программирования C/C++, технология OpenMP для параллельного исполнения активируется с помощью директив компилятора. Компиляторы для языков программирования C/C++ от фонда GNU начиная с версий 4.x учитывают эти директивы в случае передачи параметра командной строки -fopenmp. В том случае, если директивы OpenMP были учтены, компиляторы внедряют механизмы обработки потоков в местах директив и вокруг базовых блоков и других групп функций, с которыми они ассоциированы.

Так как инструмент gprof не готов был оказать нам содействие в создании отчетов для каждого из потоков, а также не имел поддержки технологии OpenMP, которую мы желали использовать, мы перешли к использованию другого инструмента. Этим инструментом был набор утилит Tuning and Analysis Utilities (TAU) [5], созданный совместными усилиями группы разработчиков, курируемой университетом штата Орегон. Существует множество профайлеров, работающих с параллельно выполняемыми потоками, причем многие из них предназначены для работы с библиотеками, реализующими интерфейс MPI (интерфейс передачи сообщений - Message Passing Interface), которые пользуются достаточной популярностью в среде разработчиков, реализующих некоторые научные вычислительные задачи в рамках своего программного обеспечения. TAU также предоставляет возможность профилирования использующих интерфейс MPI приложений, но из-за того, что интерфейс MPI в реальности не подходит для набора программного khmer в его сегодняшнем виде, мы игнорировали этот аспект функционирования TAU. Более того, TAU не является единственным доступным набором инструментов для профилирования отдельных потоков. Комбинация из возможности профилирования отдельных потоков и возможности тесной интеграции с OpenMP является одной из причин, повлиявшей на наш положительный выбор. Набор утилит TAU имеет полностью открытый исходный код и не привязан к какому-либо отдельному разработчику программного обеспечения.

В то время, как gprof опирается исключительно на вспомогательный исходный код, внедряемый в исходный код приложения в ходе компиляции (с последующим связыванием дополнительных программных компонентов с приложением), TAU также предоставляет возможность использования подобного приема наряду с другими. Дополнительными возможностями являются внедрение библиотек (в большинстве случаев применяемое для профилирования использующих интерфейс MPI приложений) и динамическое исследование бинарных файлов. Для поддержки этих дополнительных возможностей TAU предоставляет обертку исполнения, называемую tau_exec. Вспомогательный код, добавляемый в ходе компиляции, обрабатывается вспомогательным сценарием под названием tau_cxx.sh.

Набор утилит TAU требует дополнительной конфигурации для поддержки некоторых возможностей профилирования. Для тесной интеграции с использующими технологию OpenMP приложениями, к примеру, при конфигурации TAU перед сборкой должна быть включена поддержка OPARI. Аналогично, для использования счетчиков производительности, предоставляемых новыми версиями ядра Linux, он должен быть сконфигурирован и собран с поддержкой PAPI. Также после сборки набора утилит TAU вам наверняка захочется интегрировать его с вашей системой сборки для упрощения работы. Например, мы можем настроить нашу систему сборки таким образом, чтобы вспомогательный сценарий tau_cxx.sh мог использоваться в качестве компилятора для языка программирования C++ тогда, когда требуется произвести профилирование кода с использованием TAU. Если вы попытаетесь собрать и использовать набор утилит TAU, вам наверняка захочется ознакомится с документацией. Хотя утилиты из этого набора и гораздо мощнее gprof, они вовсе не являются такими же удобными для использования или интуитивными.

Исследование производительности в ручном режиме

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

Для различных участков кода нам требовалось получать различные наборы измерений. Однако, все эти различные наборы измерений имеют определенные общие черты. Во-первых, они должны по большей части хранить данные измерений времени, так как вам в общем случае понадобится собирать статистику о времени исполнения участков кода в процессе исполнения приложения. Другой важной особенностью является желание использования последовательного механизма вывода отчетов. Учитывая эти пожелания, мы создали абстрактный базовый класс IperformanceMetrics для всех наших различных наборов измерений. Класс IperformanceMetrics предоставляет некоторые вспомогательные методы: start_timers, stop_timers и timespec_diff_in_nsecs. Методы для запуска и остановки таймеров позволяют измерить как общее прошедшее время, так и время центрального процессора для каждого из потоков. Третий метод позволяет подсчитать разность между двумя представленными стандартными объектами timespec из библиотеки языка программирования C метками времени в наносекундах, что является вполне достаточной точностью для наших целей.

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


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

Продолжение статьи: Оптимизация.