профилирование кода что это

Профилирование кода — Как найти слабое звено?

DevOps Worm 2021

cprofile

Профилирование кода это попытка найти узкие места в вашем коде. Профилирование может найти самые долго выполняющиеся части вашего кода. Найдя их, вы можете оптимизировать эти части удобным вам способом. Python содержит три встроенных профайлера: cProfile, profile и hotshot. В соответствии с документацией Python, hotshot «не поддерживается, и может быть удален из Python». Модуль profile это в корне своем модуль Python, но добавляет много чего сверху в профилированные программы. Поэтому мы сфокусируемся на cProfile, который содержит интерфейс, который имитирует модуль profile.

Профилирование кода при помощи cProfile

Профилирование кода с cProfile это достаточно просто. Все что вам нужно сделать, это импортировать модуль и вызвать его функцию run. Давайте посмотрим на простой пример:

cprofile md5

Здесь мы импортировали модуль hashlib и использовали cProfile для профилирования того, что создал хеш MD5. Первая строка показывает, что в ней 4 вызова функций. Следующая строка говорит нам, в каком порядке результаты выдачи. Здесь есть несколько столбцов.

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

Источник

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

2ctv26yjae88cmtvy7nnh12bomq

Привет, я Тони Альбрехт (Tony Albrecht), инженер в Riot. Мне нравится профилировать и оптимизировать. В этой статье я расскажу об основах профилирования, а также проанализирую пример С++-кода в ходе его профилирования на Windows-машине. Мы начнём с самого простого и будем постепенно углубляться в потроха центрального процессора. Когда нам встретятся возможности оптимизировать — мы внедрим изменения, а в следующей статье разберём реальные примеры из кодовой базы игры League of Legends. Поехали!

Обзор кода

Сначала взглянем на код, который собираемся профилировать. Наша программа — это простой маленький OpenGL-рендерер, объектно ориентированное, иерархические дерево сцены (scene tree). Я находчиво назвал основной объект Object’ом — всё в сцене наследуется от одного из этих базовых классов. В нашем коде от Object’а наследуются лишь Node, Cube и Modifier.

image loader

Cube — это Object, который рендерит себя на экране в виде куба. Modifier — это Object, который «живёт» в дереве сцены и, будучи Updated, преобразует добавленные нему Object’ы. Node — это Object, который может содержать другие Object’ы.

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

image loader

Согласен, предложенный код — не лучшая реализация дерева сцены, но ничего страшного: этот код нужен именно для последующей оптимизации. По сути, это прямое портирование примера для PlayStation3®, который я написал в 2009-м для анализа производительности в работе Pitfalls of Object Oriented Programming. Можно отчасти сравнить нашу сегодняшнюю статью со статьёй 9-летней давности и посмотреть, применимы ли к современным аппаратным платформам те уроки, что мы извлекли когда-то для PS3.

Но вернёмся к нашим кубикам. На приведённой выше гифке показаны около 55 тысяч вращающихся кубиков. Обратите внимание, что я профилирую не рендеринг сцены, а только анимацию и отбрасывание (culling) при передаче на рендеринг. Библиотеки, задействованные для создания примера: Dear Imgui и Vectormath от Bullet, обе бесплатны. Для профилирования я использовал AMD Code XL и простой контрольно-измерительный (instrumented) профилировщик, на скорую руку сооружённый для этой статьи.

Прежде чем переходить к делу

Единицы измерения

Сначала я хочу обсудить измерение производительности. Зачастую в играх в качестве метрики используются кадры в секунду (FPS). Это неплохой индикатор производительности, однако он бесполезен при анализе частей кадра или сравнении улучшений от разных оптимизаций. Допустим, «игра теперь работает на 20 кадров в секунду быстрее!» — это вообще насколько быстрее?

Зависит от ситуации. Если у нас было 60 FPS (или 1000/60 = 16,666 миллисекунд на кадр), а теперь стало 80 FPS (1000/80 = 12,5 мс на кадр), то наше улучшение равно 16,666 мс – 12,5 мс = 4,166 мс на кадр. Это хороший прирост.

Но если у нас было 20 FPS, а теперь стало 40 FPS, то улучшение уже равно (1000/20 – 1000/40) = 50 мс – 25 мс = 25 мс на кадр! Это мощный прирост производительности, который может превратить игру из «неиграбельной» в «приемлемую». Проблема метрики FPS в том, что она относительна, так что мы будем всегда использовать миллисекунды. Всегда.

Проведение замеров

Существует несколько типов профилировщиков, каждый со своими достоинствами и недостатками.

Контрольно-измерительные профилировщики

Для контрольно-измерительных (instrumented) профилировщиков программист должен вручную пометить фрагмент кода, производительность которого нужно измерить. Эти профилировщики засекают и сохраняют время начала и окончания работы профилируемого фрагмента, ориентируясь на уникальные маркеры. Например:

В данном случае FT_PROFILE_FN создаёт объект, фиксирующий время своего создания, а затем и уничтожения при выпадении из области видимости. Эти моменты времени вместе с именем функции хранятся в каком-нибудь массиве для последующего анализа и визуализации. Если постараться, то можно реализовать визуализацию в коде или — чуть проще — в инструменте вроде Chrome tracing.

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

image loader

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

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

Семплирующие профилировщики

Семплирующие (sampling) профилировщики запрашивают состояние исполнения того процесса, который вы хотите профилировать. Они периодически сохраняют счётчик программы (Program Counter, PC), показывающий, какая инструкция сейчас исполняется, а также сохраняют стек, благодаря чему можно узнать, какие функции вызвала та функция, что содержит текущую инструкцию. Вся эта информация полезна, поскольку функция или строки с наибольшим количеством семплов окажутся самой медленной функцией или строками. Чем дольше работает семплирующий профилировщик, тем больше собирается семплов инструкций и стеков, что улучшает результаты.

Читайте также:  опп код по мкб

image loader

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

Специализированные профилировщики

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

Профилировщики, предназначенные для конкретных игр

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

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

Профилирование приложения без сравнительного эталона не имеет смысла, поэтому при оптимизации необходимо иметь под рукой надёжный тестовый сценарий. Это не так просто, как кажется. Современные компьютеры выполняют не одно лишь ваше приложение, а одновременно десятки, если не сотни других процессов, постоянно переключаясь между ними. То есть другие процессы могут замедлить профилируемый вами процесс в результате конкуренции за обращение к устройствам (например, несколько процессов пытаются считать с диска) или за ресурсы процессора/видеокарты. Так что для получения хорошей отправной точки вам нужно прогнать код через несколько операций профилирования, прежде чем вы хотя бы приступите к задаче. Если результаты прогонов будут сильно различаться, то придётся разобраться в причинах и избавиться от вариативности или хотя бы снизить её.

Добившись наименьшего возможного разброса результатов, не забывайте, что небольшие улучшения (меньше имеющейся вариативности) будет трудно измерить, потому что они могут затеряться в «шуме» системы. Допустим, конкретная сцена в игре отображается в диапазоне 14—18 мс на кадр, в среднем это 16 мс. Вы потратили две недели на оптимизацию какой-нибудь функции, перепрофилировали и получили 15,5 мс на кадр. Стало ли быстрее? Чтобы выяснить точно, вам придётся прогнать игру много раз, профилируя эту сцену и вычисляя среднеарифметическое значение и строя график тренда. В описанном здесь приложении мы измеряем сотни кадров и усредняем результаты, чтобы получить достаточно надёжное значение.

Кроме того, многие игры выполняются в несколько потоков, порядок которых определяется вашим оборудованием и ОС, что может привести к недетерминированному поведению или как минимум к разной длительности исполнения. Не забывайте о влиянии этих факторов.

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

Профилируем код

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

Когда я выполнил код и записал данные из измеренного профиля, то получил в Chrome://tracing такую картину:

image loader

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

image loader

Длительность исполнения одного кадра этого кода при измерении различается на пару миллисекунд, так что мы берём среднеарифметическое как минимум по нескольким сотням кадров и сравниваем с исходным эталоном. В данном случае измерено 297 кадров, среднее значение — 17,5 мс, одни кадры выполнялись до 19 мс, а другие — чуть меньше 16,5 мс, хотя в каждом из них выполняется практически одно и то же. Такова неявная вариативность кадров. Многократный прогон и сравнение результатов устойчиво дают нам около 17,5 мс, так что это значение можно считать надёжной исходной точкой.

image loader

Если отключить в коде контрольные метки и прогнать его через семплирующий профилировщик AMD CodeXL, то получим такую картину:

image loader

Если мы проанализируем пять самых «востребованных» функций, то получим:

image loader

Matrix::operator*

Если с помощью семплирующего профилировщика проанализировать код, отвечающий за умножение, то можно выяснить «стоимость» выполнения каждой строки.

image loader

К сожалению, длина кода матричного умножения — всего одна строка (ради эффективности), так что нам этот результат мало что даёт. Или всё-таки не так уж мало?

Если взглянуть на ассемблер, можно выявить пролог и эпилог функции.

image loader

Это стоимость внутренней инструкции вызова функции. В прологе задаётся новое пространство стека (ESP — текущий указатель стека, EBP — базовый указатель для текущего фрейма стека), а в эпилоге выполняется очистка и возврат. При каждом вызове функции, которая не инлайнена и использует какое-либо пространство стека (т. е. имеет локальную переменную), все эти инструкции могут быть вставлены и вызваны.

Давайте развернём остальную часть функции и посмотрим, что на самом деле выполняет матричное умножение.

image loader

Ого, куча кода! И это лишь первая страница. Полная функция занимает больше килобайта кода с 250—300 инструкциями! Проанализируем начало функции.

image loader

Строка над выделенной синим цветом занимает около 10 % общего времени выполнения. Почему она выполняется гораздо медленнее соседних? Эта MOVSS-инструкция берёт из памяти по адресу eax+34h значение с плавающей запятой и кладёт в регистр xmm4. Строкой выше то же самое делается с регистром xmm1, но гораздо быстрее. Почему?

Всё дело в промахе кеша.

Разберёмся подробнее. Семплирование отдельных инструкций применимо в самых разных ситуациях. Современные процессоры в любой момент выполняют несколько инструкций, и в течение одного тактового цикла немало инструкций может быть пересортировано (retire). Даже семплирование на основе событий может приписывать события не той инструкции. Так что при анализе семплирования ассемблера необходимо руководствоваться какой-то логикой. В нашем примере наиболее семплированная инструкция может не быть самой медленной. Мы лишь можем с определённой долей уверенности говорить о медленной работе чего-то, относящегося к этой строке. А поскольку процессор выполняет ряд MOV’ов в память и из неё, то предположим, что как раз эти MOV’ы и виноваты в низкой производительности. Чтобы удостовериться в этом, можно прогнать профиль с включённым семплированием на основе событий для промахов кеша и посмотреть на результат. Но пока что доверимся инстинктам и прогоним профиль исходя из гипотезы о промахе кеша.

Читайте также:  коды для симс 4 русалки

Пропуск кеша L3 занимает более 100 циклов (в некоторых случаях — несколько сотен циклов), а промах кеша L2 — около 40 циклов, хотя всё это сильно зависит от процессора. К примеру, x86-инструкции занимают от 1 примерно до 100 циклов, при этом большинство — менее 20 циклов (некоторые инструкции деления на некотором железе работают довольно медленно). На моём Core i7 инструкции умножения, сложения и даже деления занимали по несколько циклов. Инструкции попадают в конвейер, так что одновременно обрабатывается несколько инструкций. Это значит, что один промах кеша L3 — загрузка напрямую из памяти — по времени может занимать исполнение сотен инструкций. Проще говоря, чтение из памяти — очень медленный процесс.

image loader

Modifier::Update()

Итак, мы видим, что обращение к памяти замедляет исполнение нашего кода. Давайте вернёмся назад и посмотрим, что в коде приводит к этому обращению. Контрольно-измерительный профилировщик показывает, что Node::Update() выполняется медленно, а из отчёта семплирующего профилировщика о стеке очевидно, что функция Modifier::Update() особенно нетороплива. С этого и начнём оптимизацию.

Modifier::Update() проходит через вектор указателей к Object’ам, берёт их матрицу преобразования (transform matrix), умножает её на матрицу mTransform Modifier’а, а затем применяет это преобразование к Object’ам. В приведённом выше коде преобразование копируется из объекта в стек, умножается, а затем копируется обратно.

Внутренний слой данных этого Object’а выглядит так:

Для ясности я раскрасил записи в памяти объекта Node:

image loader

Первая запись — указатель виртуальной таблицы (virtual table pointer). Это часть реализации наследования в С++: указатель на массив указателей функций, которые выступают в роли виртуальных функций для этого конкретного полиморфного объекта. Для Object, Node, Modifier и любого класса, унаследованного от базового, существуют разные виртуальные таблицы.

Для начала отметим: вектор mObjects — это массив указателей на Object’ы, которые размещаются в памяти динамически. Итерирование по этому вектору хорошо работает с кешем (красные стрелки на иллюстрации ниже), поскольку указатели следуют один за другим. Там есть несколько промахов, но они указывают на что-то, вероятно, не адаптированное для работы с кешем. А поскольку каждый Object размещается в памяти с новым указателем, то можно сказать лишь, что наша помеха находится где-то в памяти.

image loader

Когда мы получаем указатель на Object, вызываем GetTransform() :

Эта инлайновая функция просто возвращает копию mTransform Object’а, так что предыдущая строка эквивалентна этой:

image loader

В этом фрагменте edx является расположением Object’а. А как мы знаем, mTransform начинается за 4 байта до начала объекта. Так что этот код копирует mTransform в стек (MOVUPS копирует в регистр 4 невыравненных значения с плавающей запятой). Обратите на 7 % обращений к трём MOVUPS-инструкциям. Это говорит о том, то промахи кеша также встречаются и в случае с MOV’ами. Не знаю, почему первый MOVUPS в стек занимает не столько же времени, сколько остальные. Мне кажется, «затраты» просто переносятся на последующие MOVUPS из-за особенностей конвейеризации инструкций. Но в любом случае мы получили доказательство высокой стоимости обращения к памяти, так что будем с этим работать.

Заключение

Если дочитали до конца — молодцы! Знаю, поначалу это бывает сложно, особенно если вы не знакомы с ассемблером. Но очень рекомендую найти время и посмотреть, что компиляторы делают с кодом, который они пишут. Для этого можно воспользоваться Compiler Explorer.

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

Источник

Профилирование и отладка Python

Некоторое время назад я рассказывал о «Профилировании и отладке Django». После выступления я получил много вопросов (как лично, так и по email), с парой новых знакомых мы даже выбрались в бар, чтобы обсудить важные проблемы программирования за кружечкой отменного эля, со многими людьми я продолжаю общаться до сих пор.

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

Материала много, статья получилась огромная, поэтому я решил разбить её на несколько частей:

Введение

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

Профилирование — сбор характеристик работы программы с целью их дальнейшей оптимизации.

Итак, для профилирования нам нужна работающая программа, причём работающая не совсем так, как нам хотелось бы: либо работающая слишком медленно, либо потребляющая слишком много ресурсов.

Какие же характеристики работы программы можно собирать?

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

Давайте определимся с понятием «оптимизация». Википедия подсказывает нам, что:

Оптимизация — это модификация системы для улучшения её эффективности.

Понятие «эффективность» — очень расплывчатое, и напрямую зависит от поставленной цели, например, в одних случаях программа должна работать максимально быстро, в других можно пренебречь скоростью и гораздо важнее сэкономить оперативную память или другие ресурсы (такие, как диск). Как справедливо сказал Фредерик Брукс, «серебрянной пули не существует».

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

С другой стороны, как любил повторять Дональд Кнут: «Преждевременная оптимизация — это корень всех бед».

Какая статья про оптимизацию обходится без этой цитаты? Многие полагают, что её автор — Дональд Кнут, но сам Дональд утверждает, что впервые её произнёс Энтони Хоар. Энтони же отпирается и предлагает считать высказывание «всеобщим достоянием».

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

Читайте также:  проверить водку белуга по штрих коду

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

Я не буду больше затрагивать вопрос оптимизации, поскольку, как я сказал выше, всё очень сильно зависит от поставленной задачи и каждый случай нужно разбирать отдельно. Сосредоточимся на обнаружении проблемных участков программы.

Подходы к профилированию

Существует, по крайней мере, три подхода к профилированию:

С методом пристального взгляда (и родственным ему «методом тыка») всё понятно. Просто садимся перед текстовым редактором, открываем код и думаем, где может быть проблема, пробуем починить, смотрим на результат, откатываемся. И только в редких случаях (либо при высочайшей квалификации разработчика) метод оказывается действенным.

Достоинства и недостатки этого метода:

+ не требует особых знаний и умений
– сложно оценить трудозатраты и результат

Ручное профилирование удобно использовать, когда есть обоснованное предположение об узких местах и требуется подтвердить или опровергнуть гипотезу. Либо если нам, в отличие от первого метода, нужно получить численные показатели результатов нашей оптимизации (например, функция выполнялась за 946 милисекунд, стала отрабатывать за 73 милисекунды, ускорили код в 13 раз).

Суть этого метода в следующем: перед выполнением спорного участка программы сохраняем в переменную текущее системное время (с точностью до микросекунд), а после заново получаем текущее время и вычитаем из него значение сохранённой переменной. Получаем (с достаточной для нас погрешностью) время выполнения анализируемого кода. Для достоверного результата повторяем N раз и берём среднее значение.

Достоинства и недостатки этого метода:

+ очень простое применение
+ ограниченно подходит для продакшена
– вставка «чужеродного» кода в проект
– использование возможно не всегда
– никакой информации о программе, кроме времени выполнения анализируемого участка
– анализ результатов может быть затруднительным

Профилирование с помощью инструментов помогает, когда мы (по тем или иным причинам) не знаем, отчего программа работает не так, как следует, либо когда нам лень использовать ручное профилирование и анализировать его результаты. Подробнее об инструментах в следующем разделе.

Должен заметить, что независимо от выбранного подхода, главным инструментом разработчика остаётся его мозг. Ни одна программа (пока(?)) не скажет:

Эй, да у тебя в строке 619 файла project/app.py ерунда написана! Вынеси-ка вызов той функции из цикла и будет тебе счастье. И ещё, если ты используешь кэширование, и перепишешь функцию calculate на Си, тогда быстродействие увеличится в среднем в 18 раз!

Какие бывают инструменты

Инструменты бывают двух видов (на самом деле вариантов классификации и терминологии гораздо больше, но мы ограничимся двумя):

К сожалению, я так и не смог придумать красивого названия на русском языке для «детерминистического» профайлера, поэтому я буду использовать слово «событийный». Буду благодарен, если кто-нибудь поправит меня в комментариях.

Большинство разработчиков знакомы только с событийными профайлерами, и большой неожиданностью для них оказывается тот факт, что статистический профайлер появился первым: в начале семидесятых годов прошлого столетия программисты компьютеров IBM/360 и IBM/370 ставили прерывание по таймеру, которое записывало текущее значение Program status word (PSW). Дальнейший анализ сохранённых данных позволял определить проблемные участки программы.

Первый событийный профайлер появился в конце тех же семидесятых годов, это была утилита ОС Unix prof, которая показывала время выполнения всех функций программы. Спустя несколько лет (1982 год) появилась утилита gprof, которая научилась отображать граф вызовов функций.

Статистический профайлер

Принцип работы статистического профайлера прост: через заданные (достаточно маленькие) промежутки времени берётся указатель на текущую выполняемую инструкцию и сохраняет эту информацию («семплы») для последующего изучения. Выглядит это так:
image loader
видно, функция bar()выполнялась почти в два с половиной раза дольше, чем функции foo(), baz() и какая-то безымянная инструкция.

Один из недостатков статистического профайлера заключается в том, что для получения адекватной статистики работы программы нужно провести как можно большее (в идеале — бесконечное) количество измерений с как можно меньшим интервалом. Иначе некоторые вызовы вообще могут быть не проанализированы:
image loader
например, из рисунка видно, что безымянная функция не попала в выборку.

Так же сложно оценить реальное время работы анализируемых функций. Рассмотрим ситуацию, когда функция foo() выполняется достаточно быстро, но вызывается очень часто:
image loader
и ситуацию, когда функция foo() выполняется очень долго, но вызывается лишь один раз:
image loader
результат работы статистического профайлера будет одинаковым в обоих случаях.

Тем не менее, с поиском самых «тяжёлых» и «горячих» мест программы статистический профайлер справляется великолепно, а его минимальное влияние на анализируемую программу (и, как следствие, пригодность к использованию в продакшене) перечёркивает все минусы. К тому же Python позволяет получить полный stacktrace для кода при семплировании и его анализ позволяет получать более полную и подробную картину.

Достоинства и недостатки статистического профайлера:

+ можно пускать в продакшен (влияние на анализируемую программу минимально)
– получаем далеко не всю информация о коде (фактически только «hot spots»)
– возможно некорректное интерпретирование результата
– требуется длительное время для сбора адекватной статистики
– мало инструментов для анализа

Событийный профайлер

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

Самый важный недостаток таких профайлеров прямо следует из принципа их работы: поскольку мы вмешиваемся в анализируемую программу на каждом шагу, процесс её выполнения может (и будет) сильно отличаться от «обычных» условий работы (прям как в квантовой механике). Так, например, в некоторых случаях возможно замедление работы программы в два и более раз. Конечно, в продакшен выпускать такое можно только в случае отчаяния и полной безысходности.

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

Достоинства и недостатки событийных профайлеров:
+ не требуется изменения кода
+ получаем всю информаци о работе программы
+ огромное количество инструментов
– в некоторых случаях профайлер меняет поведение программы
– очень медленно
– практически непригодно для продакшена

В следующей статье мы на практике разберём ручное профилирование и статистические профайлеры. Оставайтесь на связи =)

Источник

Поделиться с друзьями
admin
Здоровый образ жизни: советы и рекомендации
Adblock
detector