Высокопроизводительный код на платформе .NET. 2-е издание - Бен Уотсон - E-Book

Высокопроизводительный код на платформе .NET. 2-е издание E-Book

Бен Уотсон

0,0

Beschreibung

Хотите выжать из вашего кода на .NET максимум производительности? Эта книга развеивает мифы о CLR, рассказывает, как писать код, который будет просто летать. Воспользуйтесь ценнейшим опытом специалиста, участвовавшего в разработке одной из крупнейших .NET-систем в мире. В этом издании перечислены все достижения и улучшения, внесенные в .NET за последние несколько лет, в нем также значительно расширен охват инструментов, содержатся дополнительные темы и руководства. Вот лишь некоторые из тем, рассматриваемых в книге: •Различные способы анализа куч и выявления проблем, связанных с памятью. •Профессиональное использование Visual Studio и других инструментов. •Дополнительные сведения об эталонном тестировании. •Новые варианты настройки сборки мусора. •Приемы предварительной подготовки кода. •Более подробный анализ LINQ. •Советы, касающиеся функциональных областей высокого уровня, таких как ASP.NET, ADO.NET и WPF. •Новый функционал платформы .NET, включая возвращения по ссылке, структурные кортежи и SIMD. •Профилирование с использованием нескольких инструментов. •Эффективное использование библиотеки Task Parallel. •Рекомендуемые и не рекомендуемые к использованию API. •Счетчики производительности и инструментарий ETW-событий. •Формирование команды, нацеленной на достижение высокой производительности.

Sie lesen das E-Book in den Legimi-Apps auf:

Android
iOS
von Legimi
zertifizierten E-Readern
Kindle™-E-Readern
(für ausgewählte Pakete)

Seitenzahl: 490

Veröffentlichungsjahr: 2022

Das E-Book (TTS) können Sie hören im Abo „Legimi Premium” in Legimi-Apps auf:

Android
iOS
Bewertungen
0,0
0
0
0
0
0
Mehr Informationen
Mehr Informationen
Legimi prüft nicht, ob Rezensionen von Nutzern stammen, die den betreffenden Titel tatsächlich gekauft oder gelesen/gehört haben. Wir entfernen aber gefälschte Rezensionen.



Бен Уотсон
Высокопроизводительный код на платформе .NET. 2-е издание

Научный редактор М. Сагалович

Переводчик Н. Вильчинский

Технический редактор Н. Рощина

Литературный редактор Н. Рощина

Художник Н. Гринчик

Корректоры О. Андриевич, Н. Гринчик, Е. Павлович

Верстка Г. Блинов

Бен Уотсон

Высокопроизводительный код на платформе .NET. 2-е издание. — СПб.: Питер, 2021.

ISBN 978-5-4461-0911-1

© ООО Издательство "Питер", 2021

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

Оглавление

Предисловие
Об авторе
Контактная информация
Благодарности
От издательства
Введение во второе издание
Введение
Цель этой книги
В чем смысл выбора управляемого кода
Работает ли управляемый код медленнее нативного?
Стоит ли овчинка выделки?
Я что, теряю контроль?
Работа с CLR, а не против нее
Уровни оптимизации
Коварная соблазнительность простоты
Хронология совершенствования производительности среды .NET
.NET Core
Учебный исходный код
Глава 1. Измерение производительности и инструменты
Выбор предмета измерения
Преждевременная оптимизация
Сравнение усредненных и процентных показателей
Эталонное тестирование
Полезные инструменты
Издержки измерений
Резюме
Глава 2. Управление памятью
Выделение памяти
Операция сборки мусора
Параметры конфигурации
Советы по повышению производительности
Исследование памяти и сборки мусора
Резюме
Глава 3. JIT-компиляция
Преимущества JIT-компиляции
JIT в действии
JIT-оптимизации
Сокращение времени JIT-компиляции и запуска
Оптимизация JIT-компиляции с помощью профилирования (Multicore JIT)
Когда следует применять NGEN
.NET Native
Настраиваемая предварительная подготовка
Когда JIT-компиляция не может составить конкуренцию
Исследование поведения JIT-компилятора
Резюме
Глава 4. Асинхронное программирование
Пул потоков
Библиотека распараллеливания задач
Среда TPL Dataflow
Параллельно выполняемые циклы
Советы по повышению производительности
Синхронизация потоков и блокировки
Исследование потоков и конфликтов
Резюме
Глава 5. Общие подходы к написанию кода и классов
Классы и структуры
Кортежи
Диспетчеризация интерфейсов
Избегайте упаковки
Возвращения по ссылке (ref) и локальные значения
for или foreach
Приведение типов
P/Invoke
Делегаты
Исключения
dynamic
Отражение
Генерация кода
Предварительная обработка
Исследование проблем производительности
Резюме
Глава 6. Использование среды .NET Framework
Разберитесь с каждым вызываемым API
Множество API для решения одних и тех же задач
Коллекции
Строки
Избегайте использования API, выдающих исключения при обычных обстоятельствах
Избегайте использования API, выделяющих память из кучи больших объектов
Применение ленивой инициализации
Удивительно высокие издержки от использования перечислений
Учет времени
Регулярные выражения
LINQ
Чтение и запись файлов
Оптимизация настроек HTTP и сетевых соединений
SIMD
Исследование причин возникновения проблем с производительностью
Резюме
Глава 7. Счетчики производительности
Использование существующих счетчиков
Создание пользовательского счетчика
Резюме
Глава 8. ETW-события
Определение событий
Потребление пользовательских событий в PerfView
Создание собственного слушателя ETW-событий
Получение подробных данных об EventSource
Потребление событий CLR и системы
Пользовательские аналитические расширения PerfView
Резюме
Глава 9. Безопасность и анализ кода
Представление об операционной системе, API и оборудовании
Ограничение использования API в определенных областях кода
Выполняйте централизацию и абстрагирование сложного и важного для повышения производительности кода
Изолируйте неуправляемый и небезопасный код
Отдавайте приоритет ясности кода, а не получению высокой производительности, пока нет веских причин для обратного
Резюме
1Глава 0. Формирование команды, нацеленной на достижение высокой производительности
Выявление областей, требующих особо высокой производительности
Эффективное тестирование
Инфраструктура и автоматизация для оценки производительности
Доверяйте только конкретным числовым показателям
Эффективная система просмотра кода
Обучение
Резюме
Приложение А. Начало работы над повышением производительности приложения
Определение метрик
Анализ использования центрального процессора
Анализ использования памяти
Анализ JIT-компиляции
Анализ производительности в асинхронном режиме
Приложение Б. Увеличение производительности на более высоком уровне
ASP.NET
ADO.NET
WPF
Приложение В. Нотация «“O” большое»
«O» большое
Самые распространенные алгоритмы и их сложность
Приложение Г. Библиография
Ценные источники информации
Люди и блоги

Предисловие

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

Уже более десяти лет я работаю архитектором производительности .NET Runtime. Проще говоря, моя задача — убедить людей, использующих C# и среду выполнения кода .NET, что их вполне устраивает производительность созданного ими кода. Часть этой задачи — поиск мест внутри .NET Runtime или библиотек этой среды, работающих неэффективно, и внесение в них исправлений, но это не самое трудное из того, чем приходится заниматься. Сложнее всего то, что в 90 % случаев производительность приложений определяется не особенностями реализации среды выполнения (например, качеством генерации кода, компиляцией ко времени применения, сборкой мусора или функционированием библиотек классов), а тем, что находится в ведении разработчика приложения (например, архитектурой приложения, выбором структур данных и алгоритмов и просто ошибками в коде). Таким образом, моя работа куда больше связана с обучением, чем с программированием.

Значительная часть работы заключается в проведении бесед и написании статей, и в основном я консультирую другие команды, которым нужна помощь в ускорении работы их программ. Именно в такой роли я шесть лет назад впервые встретился с Беном Уотсоном. Он был тем самым представителем Bing-команды, который всегда задавал необычные вопросы (и находил ошибки в нашем коде, а не в своем). Бен явно был нашим, из разряда борцов за высокую производительность. Вы не представляете, насколько это редкое явление. Наверное, 80 % программистов пройдут основную часть своего карьерного пути, имея весьма смутное представление о производительности создаваемого ими кода. Возможно, 10 % проявят достаточное внимание к вопросам производительности по мере освоения работы со средствами анализа, подобными профилировщикам. Тот факт, что вы читаете эту книгу (и это предисловие!), говорит о том, что вы относитесь к небольшой группе людей, кто реально радеет за высокую производительность и действительно хочет, чтобы она постоянно повышалась. Бен идет еще дальше: его не только интересует все, что связано с производительностью, но он также заботится о ее достижении настолько серьезно, что нашел время для написания этой книги. Он относится к тем специалистам, которые составляют всего 0,0001 % от их общего количества. Вам предоставлена возможность учиться у самых лучших профессионалов.

Это весьма важная книга. На своем веку я сталкивался с множеством проблем, связанных с производительностью, и, как уже упоминалось, в 90 % случаев они возникали в самом приложении. Это означает, что решение таких проблем в ваших руках. В качестве предисловия к некоторым моим выступлениям о производительности я часто привожу следующую аналогию: представьте, что вы уже написали 10 000 строк нового кода для какого-то приложения и вам даже удалось этот код скомпилировать, но само приложение вы еще не запустили. Какова вероятность того, что код не содержит ошибок? Основная часть аудитории скажет: ноль. И будет совершенно права. Все, кто когда-либо занимался программированием, знают, что обрести уверенность в стабильной работе программы можно, лишь потратив много времени на эксплуатацию приложения и устранение проблем. Программирование — занятие непростое, и нужный результат получается путем последовательной доработки кода. Итак, представьте теперь, что вы потратили время на отладку программы в 10 000 строк, после чего она, казалось бы, заработала как надо. Но перед вашим приложением стоит весьма непростая цель — достичь высокой производительности. Какова вероятность того, что у него нет проблем с производительностью? Программисты — народ неглупый, следовательно, вы быстро поймете, что она стремится к нулю. Точно так же, как компилятор не в состоянии выявить множество проблем времени выполнения, обычное функциональное тестирование не позволяет отследить множество вопросов, связанных с производительностью. Поэтому определенный объем знаний о производительности необходим всем, что и предоставляет данная книга.

Еще одна тревожная истина, касающаяся производительности, гласит: самые трудноустранимые просчеты закладываются в приложение на ранней стадии проектирования. Дело в том, что именно тогда выбирается базовое представление обрабатываемых данных, и оно накладывает жесткие ограничения на производительность. Я сбился со счета, сколько раз приходилось консультировать людей, выбравших неудачное представление (например, XML, или JSON, или базу данных) для данных, играющих важную роль в достижении их приложениями высокой производительности. Они обращались ко мне за помощью на слишком позднем этапе производственного цикла, надеясь, что я сотворю чудо, устранив возникшие у них проблемы с производительностью. Разумеется, я помогал им измерить ее и обычно находил что-то, что можно исправить, но добиться существенных успехов это не позволяло, поскольку требовалось изменить базовое представление, что в конце производственного цикла было слишком затратно и рискованно. В результате конечный продукт работал далеко не так быстро, как мог бы, если бы проблемы с производительностью осознали своевременно.

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

1. Иметь план достижения высокой производительности.

2. Постоянно проводить измерения, измерения и еще раз измерения.

В реальности пункт «Иметь план достижения высокой производительности» сводится к стремлению серьезно относиться к вопросам производительности. Это означает, что нужно определить, какие метрики вы будете использовать (обычно это затраченное время, иногда что-то другое), и выявить основные операции, потребляющие наибольшее число ресурсов, охватываемых этой метрикой (обычно это операции с большими объемами данных, на выполнение которых, вероятно, уйдет значительная часть времени работы приложения). На самой ранней стадии проекта — перед тем как принять любое серьезное проектировочное решение — следует подумать о том, какой производительности предполагается достичь, и измерить этот показатель для какого-либо кода, например созданных ранее аналогичных приложений или прототипов вашего решения. Это или придаст вам уверенности в достижимости поставленных целей, или позволит понять, что добиться желаемого может оказаться нелегко и для поиска более удачного решения понадобятся глубже проработанные прототипы и проведение более тонких экспериментов. Я не говорю о каких-то космических технологиях. На реализацию некоторых планов повышения производительности уходят буквально минуты. Главное — проделать это на самой ранней стадии проектирования, чтобы планы достижения высокой производительности могли повлиять на принятие проектировочных решений, подобных форме представления данных, в самом начале разработки.

Пункт «Постоянно проводить измерения, измерения и еще раз измерения» просто подчеркивает, на что вы будете затрачивать основную часть времени (наряду с толкованием результатов). Как сказал бы Аластор Муди по прозвищу Бешеный Глаз (Alastor «Mad-Eye» Moody), нам нужна постоянная бдительность. Производительность может быть утрачена практически на любом этапе производственного цикла, от проектирования до поддержки готового продукта, и предотвратить это можно, только выполняя все новые и новые измерения, не позволяющие выбиться из колеи. Повторюсь: здесь не нужны космические технологии, должно быть просто желание делать это постоянно, предпочтительно автоматизировав необходимые действия.

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

Ценность данной книги в том, что она поможет справиться с этой сложностью. Обеспечение производительности может показаться неподъемной задачей. Ведь столь многое может быть измерено и существует такое разнообразие инструментов для проведения этих измерений, что зачастую не вполне понятно, какие именно измерения наиболее полезны и как правильно соотнести их друг с другом. Для начала эта книга поможет с решением базовой задачи — обозначением целей, важных именно для вас. Вас также снабдят небольшим набором инструментов и метрик, ценность которых проверена временем, что позволит вам двигаться дальше в верном направлении. На этой прочной основе в книге начинается «раздевание луковицы», позволяющее вникнуть в детали рассматриваемых тем, играющих важную роль при решении проблем производительности для целого ряда приложений. К ним относятся управление памятью (сборка мусора), компиляция на лету (just in time, JIT) и асинхронное программирование. Таким образом, вы получите все необходимые детали (среды выполнения весьма сложны, и иногда эта сложность проявляется и серьезно влияет на производительность), к тому же в общей структуре, позволяющей связывать эти детали с тем, что представляет для вас истинный интерес, — с целями именно вашего приложения.

Теперь дадим слово Бену, и он умело растолкует вам все остальное. Мне же хотелось не просветить вас, а мотивировать на чтение книги. Исследование вопросов повышения производительности — весьма сложная область компьютерной науки, и без того непростой. Чтобы набраться опыта, нужны время и настойчивость в достижении поставленной цели. Я обращаюсь к читателям не для приукрашивания действительности, а чтобы сказать, что дело того стоит. Производительность весьма важна. Я могу практически гарантировать, что при широком использовании вашего приложения его производительность будет важна. Учитывая важность данного вопроса, можно считать чуть ли не преступлением то, что крайне мало сведущих в нем специалистов, обладающих достаточным мастерством для создания высокопроизводительных приложений на системной основе. Теперь вы читаете эти строки, чтобы влиться в ряды элитной группы нашего сообщества. И эта книга существенно упростит решение данной задачи.

Молодежь не понимает, как ей повезло!

Вэнс Моррисон (Vance Morrison), Performance Architect среды выполнения .NET, Microsoft

Об авторе

Бен Уотсон (Ben Watson) с 2008 года является программистом компании Microsoft. В команде, работающей над платформой Bing, он создал на основе платформы .NET высокопроизводительные серверные приложения, которые способны обрабатывать большие объемы информации и отличаются высокой скоростью реакции на запросы от тысяч машин и миллионов клиентов. Эти приложения можно отнести к самым совершенным в мире. На досуге Бен читает, слушает музыку, гуляет и общается с женой Летицией и детьми Эммой и Мэтью. Они живут недалеко от Сиэтла (штат Вашингтон, США).

Контактная информация

Бен Уотсон (Ben Watson)

Электронная почта: [email protected].

Веб-сайт: http://www.writinghighperf.net.

Блог: http://www.philosophicalgeek.com.

LinkedIn: https://www.linkedin.com/in/benmwatson.

Twitter: https://twitter.com/benmwatson.

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

Благодарности

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

Спасибо Клэр Уотсон (Claire Watson) за разработку красивой обложки для обоих изданий книги.

Я благодарен своему наставнику Майку Магрудеру (Mike Magruder), который, наверное, чаще, чем кто-либо другой, читал эту книгу. Он был техническим редактором первого издания, а уже будучи на пенсии, нашел время, чтобы вернуться к подробностям устройства среды .NET во втором издании.

Спасибо читателям предварительной версии книги, чье неоценимое внимание к ее формулировкам и темам позволило обнаружить опечатки, мои упущения и многие другие недочеты. Это Абхинав Джайн (Abhinav Jain), Майк Магрудер (Mike Magruder), Чад Парри (Chad Parry), Брайан Расмуссен (Brian Rasmussen) и Мэтт Уоррен (Matt Warren). Именно благодаря им книга стала еще лучше.

Благодарю Вэнса Моррисона (Vance Morrison), прочитавшего раннюю версию книги и написавшего замечательное предисловие к этому изданию.

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

От издательства

Ваши замечания, предложения, вопросы отправляйте по адресу [email protected] (­издательство «Питер», компьютерная редакция).

Мы будем рады узнать ваше мнение!

На сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.

Введение во второе издание

Со времени выхода первого издания книги Writing High-Performance .NET Code основы достижения высокой производительности в среде .NET особых изменений не претерпели. Правила оптимизации сборки мусора остались практически неизменными. JIT-компиляция, прибавив в производительности, в основном сохранила прежнее поведение. Но за это время вышло уже пять доработанных версий среды .NET, и они заслуживают упоминания там, где это уместно.

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

• Объем увеличился на 50 %.

• Учтены отзывы сотен читателей.

• Появилось новое предисловие от Performance Architect .NET Вэнса Моррисона.

• Приведены десятки новых примеров и фрагментов учебного кода.

• Обновлены схемы и графики.

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

• Рассмотрено большее количество инструментальных аналитических средств.

• Значительно расширена область использования Visual Studio для анализа производительности среды .NET.

• Добавлены многочисленные примеры анализа с использованием набора API Microsoft.Diagnostics.Runtime (CLR MD).

• В некоторые учебные проекты добавлен материал по эталонному тестированию (бенчмаркингу) и применению популярных сред такого тестирования.

• Появились новые разделы, посвященные особенностям CLR и .NET Framework, имеющим отношение к производительности.

• Больше внимания уделено сборке мусора, добавлена новая информация о составлении пула, stack-alloc, финализации, слабых ссылках, поиске утечек памяти и многом другом.

• Подробнее рассмотрены различные технологии предварительной подготовки кода.

• Добавлена информация о TPL и новый раздел о TPL Dataflow.

• Рассмотрены возвращения по ссылке и локальные переменные.

• Гораздо подробнее рассмотрены коллекции, включая исходный объем, сортировку и сравнение ключей.

• Дан подробный анализ затрат на LINQ.

• Приведены примеры SIMD-алгоритмов.

• Показаны приемы создания автоматизированных анализаторов кода и средств его исправления.

• Добавлено приложение, в котором даются советы по увеличению производительности на более высоком уровне для ADO.NET, ASP.NET и WPF.

...И многое другое!

Уверен, что даже тем, кто прочитал первое издание, стоит найти время и ознакомиться со вторым.

Глава 1. Измерение производительности и инструменты

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

Выбор предмета измерения

Перед тем как принять решение о предмете измерения, следует усвоить набор требований к производительности. Эти требования должны носить довольно общий характер, чтобы не предписывать конкретную реализацию, но в то же время быть вполне определенными и поддаваться измерениям. Они должны основываться на реальных положениях, даже если пока вы не знаете, как добиться их выполнения. Требования станут управлять тем, какие показатели нужно будет собрать. Перед сбором числовых значений следует разобраться в том, что именно мы намерены измерять. Звучит это как само собой разумеющееся, но на самом деле вопрос гораздо шире, чем можно себе представить. Возьмем, к примеру, память. Разумеется, вам захочется измерить объем потребляемой памяти и свести его к минимуму. О какой именно памяти идет речь? Частном рабочем наборе? Запрошенном объеме памяти? Страничном пуле? Пиковом рабочем наборе? Объеме кучи .NET-среды? Объеме кучи крупных объектов? Об объемах куч отдельно взятых процессоров, обеспечивающих их сбалансированность? О каком-то ином варианте? Нужен ли для отслеживания объема потребляемой памяти по времени усредненный почасовой показатель или же необходим пиковый показатель? Коррелируется ли объем потребляемой памяти с объемом рабочей нагрузки при вычислениях? Как видите, навскидку легко набирается десяток и более показателей, относящихся к одной лишь памяти. А мы еще не касались частных куч или профилирования приложений с целью определения того, какие разновидности объектов потребляют память!

Характеризуя потребности в измерениях, нужно формулировать запросы как можно конкретнее.

История

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

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

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

Плохой пример: «Пользовательский интерфейс должен быть отзывчивым».

Хороший пример: «Не должно быть операций, способных заблокировать поток пользовательского интерфейса более чем на 20 мс».

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

Плохой пример: «Объем памяти должен быть менее 1 Гбайт».

Хороший пример: «Объем памяти, потребляемый рабочим набором, не должен превышать 1 Гбайт при пиковой нагрузке 100 запросов в секунду».

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

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

Важную роль играет и планирование мощностей. При разработке вашей системы и планировании измерений производительности весьма полезно решить, какой будет ее оптимальная теоретическая производительность. Если можно исключить все источники издержек, в том числе сборку мусора, JIT-компиляцию, прерывания потока и все, что вы считаете таковыми, то что останется для выполнения работы, для которой приложение предназначено? Какие теоретические ограничения с точки зрения рабочей нагрузки, потребления памяти, использования центрального процессора и внутренней синхронизации вы можете определить? Зачастую это зависит от задействованного оборудования и операционной системы. Например, при наличии 16-процессорного сервера с 64 Гбайт оперативной памяти с двумя сетевыми каналами по 10 Гбайт возникает мысль о пороге распараллеливания и о том, сколько данных можно сохранить в памяти и сколько — передать по каналу в каждую секунду. Это поможет спланировать нужное количество машин данного типа, если одной окажется недостаточно.

Преждевременная оптимизация

Возможно, вы знакомы с высказыванием Дональда Кнута (Donald Knuth): «Прежде­временная оптимизация — корень всех зол». Смысл этой цитаты заключается в определении тех областей вашей программы, которые действительно важно оптимизировать. Это отсылает нас к закону Амдала, в котором описывается теоретически максимальное ускорение работы программного средства путем его оптимизации, в частности способ применения оптимизации к последовательно выполняемым программам и выбор оптимизируемых частей программы. Время, потраченное на микрооптимизацию того кода, который не вносил существенного вклада в общую неэффективность, по большому счету считается затраченным впустую. Это положение верно для микрооптимизаций на уровне кода и может быть применимо и для более высоких уровней вашей архитектуры. Конечно, следует продумывать создаваемую архитектуру и по мере разработки разбираться в ее ограничениях, иначе можно упустить что-то важное и сильно затрудняющее работу приложения. Но многие из этих ограничений на поверку окажутся не столь важны для системы (или по крайней мере вы пока не сможете оценить их важность). Перепроектировать существующее приложение с нуля, конечно, можно, но это будет значительно дороже, чем изначально спроектировать его правильно. При разработке архитектуры крупной системы зачастую единственный способ избежать ловушки преждевременной оптимизации — это применение опыта разработки и изучение архитектуры аналогичных или эталонных систем. В любом случае определение целей достижения производительности должно опережать проектные решения. Производительность, подобно безопасности и многим другим аспектам разработки программного обеспечения, не может играть второстепенную роль и с самого начала должна быть четко выраженной целью.

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

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

Сравнение усредненных и процентных показателей

Рассматривая измеряемые числовые метрики, следует решить, какой вид статистических показателей будет наиболее подходящим. Большинство специалистов изначально полагаются на усредненные показатели. Конечно, в большинстве случаев они играют важную роль, но не следует сбрасывать со счетов и процентные показатели (процентили). При наличии требований к доступности вам почти наверняка понадобятся цели, выраженные в процентилях, например: «Среднее время задержки запросов к базе данных должно быть менее 10 мс. А 95%-ный процентиль задержек запросов к базе данных должен быть менее 100 мс».

Если вам неизвестно это понятие, не расстраивайтесь: в нем нет ничего сложного. Если взять 100 измерений чего угодно и отсортировать их, то 95%-ная запись в списке и является значением 95%-ного процентиля этого набора данных. 95%-ный процентиль свидетельствует о том, что 95 % значений в выборке имеет это или меньшее значение. Или же 5 % запросов имеют значение выше этого.

Общая формула для вычисления индекса отсортированного списка имеет следующий вид:

P

100

N

,

где P — процентиль; N — длина списка.

Рассмотрим серию измерений времени паузы при сборке мусора нулевого поколения в миллисекундах со следующими значениями (для удобства они были заранее отсортированы):

1, 2, 2, 4, 5, 5, 8, 10, 10, 11, 11, 11, 15, 23, 24, 25, 50, 87.

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

Здесь также демонстрируется, что медианное значение (50%-ный процентиль) может значительно отличаться от среднего, которое подвергается весьма сильному влиянию со стороны значений более высоких процентилей.

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

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

Я начал с того, что 95%-ный процентиль показанного ранее набора данных составлял 50 мс. Технически это так, но в данном случае польза от этой информации сомнительна, поскольку данных недостаточно, чтобы этот результат имел хоть какую-то статистическую значимость, и, в сущности, данный показатель может носить случайный характер. Для определения нужного числа значений в выборке воспользуйтесь следующим эмпирическим правилом: необходимо, чтобы размер выборки был на один порядок больше целевого процентиля. Для процентилей в диапазоне 0–99 нужны минимум 100 значений. Для 99,9%-ного процентиля — 1000, для 99,99%-ного процентиля — 10 000 и т.д. В большинстве случаев это правило работает, но если вы заинтересованы в определении фактического количества значений с математической точки зрения, то изучите науку определения объема выборки глубже.

Точнее говоря, потенциальная ошибка зависит от квадратного корня из числа значений в выборке. Например, 100 значений приводят к диапазону ошибок 90–100, или 10 % ошибок, 1000 — к диапазону 969–1031, или 3 % ошибок.

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

Эталонное тестирование

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

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

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

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

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

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

Кроме аппаратной изолированности, следует рассмотреть и ряд других факторов.

• Код должен быть предварительно JIT-компилирован. Первоначальный запуск метода занимает гораздо больше времени, чем последующие запуски.

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

• Изоляция. Если окажется, что в процессе эталонного тестирования запущены иные «тяжелые» процессы, измерения могут исказиться.

• Резкие отклонения. Нужно делать скидку на резкие статистические отклонения в измерениях, которые, вероятно, не следует брать в расчет. Определение того, что считать резким отклонением, а что — нормой, — задача непростая.

• Узкая сфокусированность. Время центрального процессора — важный показатель, но не менее важны метрики выделения памяти, ввода-вывода, блокировки потоков и многие другие.

• Различие между кодом, предназначенным для выпуска (Release mode), и кодом для отладки (Debug mode).