Привет, Хабр! Замер производительности кода — не самое простое упражнение для разработчика. Приходится решать кучу сложностей: разбираться с методом, создавать правильные условия. И всё равно можно получить результат с погрешностью, потому что любой метод «не бесплатный» и требует ресурсов процессора.
Меня зовут Александр Певненко, я Java-разработчик в СберТехе. Вместе с командой мы развиваем Platform V DataSpace. Это облачный сервис, который упрощает и ускоряет разработку приложений, используя концепцию Backend-as-a-Service (BaaS) для хранения и управления данными. Я расскажу про наш способ замера производительности кода с помощью бенчмарков. Рассматривать метод будем на примере оптимизации кода в Platform V Dataspace, которая помогла сократить время вызова метода в 16 раз.
В статье я буду пользоваться языком Java, Python для построения графиков и набором библиотек JMH — они также адаптированы для Kotlin, Scala и т.д.
Зачем вообще измерять производительность кода…
…если вроде бы всё и так работает нормально? На самом деле, помимо очевидных выгод в виде скорости или стабильности есть ещё одна причина: оптимизацию можно рассматривать как важную часть культуры CI/CD. В небольших проектах эти параметры могут и не быть критичными. Зато практически все Agile-команды сегодня работают с DevOps-практиками и осознают ценность непрерывной поставки.
Пока проект «молодой», развёртывание и тестирование может быть относительно простым. Но как только модуль вырастает за рамки агента, выполняющего сборку, или на локальную сборку тестов начинает уходить по два часа, в головы разработчиков приходят светлые мысли: «А может, стоит уменьшить код? Заняться производительностью? Вдруг какие-то методы раздуты до небывалых размеров?» Именно здесь на помощь приходит оптимизация кода. Она становится частью непрерывной интеграции и следующим шагом в развитии DevOps.
В нашем случае важны все аспекты. Скорость — потому что Platform V DataSpace поставляется в составе облачной платформы Platform V, которая лежит в основе большинства продуктов Сбера. Оптимизация DevOps — из-за того, что продукт быстро растёт и важно обеспечивать непрерывную поставку функциональности в промышленную эксплуатацию.
Как замерить производительность: методы и сложности
Вёрнемся к нашему примеру. В одном из проектов Platform V DataSpace вызов метода занимал очень много времени. При этом алгоритм построен так, что избежать сложностей и вызывать метод «малой кровью» не получалось.
При пристальном рассмотрении выяснилось, что код вызывал один и тот же метод несколько раз, из-за чего растягивался timestamp. Для оптимизации нужно было оценить точную длительность работы метода.
Классический подход к замеру оптимальности кода — нотация «О» большое, О(). Но с этим методом есть одна сложность: он не позволяет замерять код «в боевых условиях». Даже если приложить максимум усилий, провести оценку с помощью нотации и обеспечить видимую оптимальность всех блоков кода по отдельности, можно получить неоптимальное решение (вспоминаем про «жадные алгоритмы», когда производительность не композируется). На результат могут повлиять разные факторы: стили программирования, типы используемых данных, особенности процессора. Тогда мы решили обратиться к альтернативному решению — бенчмаркам.
Бенчмарк — тест для оценки длительности работы метода. Он хорош тем, что позволяет замерять скорость работы алгоритма с учетом всех внешних факторов и на реальном оборудовании. В основе любого бенчмарка лежит системное время работы процессора и расчёт длительности выполнения блока кода. Чаще всего для этого используется метод System.nanoTime()
, у которого, как оказалось, тоже есть свои особенности. Дело в том, что сам по себе метод системного взятия времени неизбежно даёт погрешность, даже если мы сделаем так:
Void checkTime(){
Long oldTime = System.nanoTime();
work();
return System.nanoTime() – oldTime;
}
Погрешность связана с тем, что сам по себе метод не «бесплатный»: его стоимость равна стоимости ресурсов машины, которые она затрачивает на расчёт. Избежать этого не получится, тут как в квантовой механике: если наблюдатель влезает в квантовый мир, он уже вносит погрешность самим фактом наблюдения.
Возникают вопросы:
Получается, мы никак не избежим погрешности при вычислении «стоимости» метода
work()
?Что считать baseline и на основе чего его строить?
Как «сжечь» время при замере?
Но после подробного изучения метода оказалось: нет, погрешности избежать можно. Для этого есть несколько вариантов. Самый простой — использовать библиотеки JMH для Java.
JMH: чем так хороши и почему не подошли нам
JMH — это набор библиотек для тестирования производительности небольших функций. Если использовать библиотеки, то можно избежать погрешности благодаря тому, что мы:
узнаём латентность — время на вызов
System.nanoTime()
,измеряем гранулярность метода — разрешающую способность, минимальную ненулевую разницу между вызовами метода
System.nanoTime()
.
Это позволит нам получить значение, которое будет коррелировать с длительностью исполнения метода взятия времени. Вроде бы всё просто: JMH сама вычисляет латентность и гранулярность. Но расслабляться всё равно не стоит. На разных ОС измерение времени с помощью латентности и гранулярности может быть разным, поэтому, вызывая этот метод при большом количестве потоков, нужно быть осторожным.
В нашем случае использовать только бенчмарки JMH было невозможно из-за внутренних ограничений и требований к процессу. Поэтому пришлось искать третий вариант — измерять производительность кода и писать бенчмарки самостоятельно.
Тестирование оптимальности кода в «боевых условиях»
Ещё один способ избежать погрешности — измерить «стоимость» System.nanoTime()
. Это позволит нам получить значение, которое будет коррелировать с длительностью исполнения метода взятия времени.
Для решения проблемы с повторным вызовом метода я подготовил самописный бенчмарк и попробовал вычесть baseline до оптимизации и после. Затем сравнил, сколько времени требовалось на исполнение кода до и после доработок. Чтобы работать было проще, собрал графический интерфейс для визуализации на Python. Вот что получилось:
Аналогичные измерения после оптимизации давали видимый результат. На графике ниже — измерения до оптимизации (верхние линии) и после (нижние):
В итоге длительность вызова метода сократилась в 16 раз. На графиках это значение может отображаться с погрешностью, которая допустима при визуализации замеров, проводимых в «боевых условиях». Но само значение выигрыша действительное, так как погрешность двух замеров вычитается сама из себя, а нам необходимо относительное значение, а не абсолютное.
Выводы
Измерять производительность кода стоит хотя бы из любопытства, а лучше для того, чтобы повысить скорость продукта и упростить развёртывание. Бенчмарки — отличное решение для этого. В нашем случае самописные бенчмарки помогли серьёзно сократить длительность вызова System.nanoTime()
. Сейчас работаем над тем, чтобы в большинстве проектов тестировать оптимальность всей системы ради снижения количества потенциальных проблем.
Писать собственные бенчмарки и устранять погрешности с помощью вычитания «стоимости» метода — удел не для каждого. Тем более, что в JMH все эти проблемы решаются автоматически, измерением латентности и гранулярности. Так что вполне можно пользоваться такими решениями: работы намного меньше, а польза очевидна.
Комментарии (12)
capt_Rimmer
00.00.0000 00:00/бубнит Лучше бы нормальные bitrix модули для СБП и интернет-эквайринга своего же сделали...
akhmelev
00.00.0000 00:00+8Замеряй и ускоряй: как мы сократили время вызова метода в Java-коде в 16 раз - э... действительно: как?
даже нет (голосом Коляна): да как так то?
ChatGPT что ли?
Предлагаю переименовать, пока не поздно:
Суровые трудовые будни или для чего в Сбере применяют System.nanotime() - вот так было бы пожалуй информативнее.
gazkom
00.00.0000 00:00+5В одном из проектов Platform V DataSpace вызов метода занимал очень много времени.
Что занимало много времени? Вызов? Может метод долго работал, а не вызывался долго?
Для оптимизации нужно было оценить точную длительность работы метода.
Для оптимизации, очевидно, нужно уменьшить время работы метода. А еще лучше перестать его вызывать несколько раз. А оценить точную длительность работы метода нужно для того, чтобы решить, нужно ли его вообще менять и если да, то чтобы оценить результат доработки.
PaulIsh
00.00.0000 00:00Можно подключить трассировки, например, elastic APM и там увидеть все задержки в боевых условиях вместо изобретения своих методов замера и графиков.
Vest
00.00.0000 00:00+1Что мешало включить профайлинг во время выполнения запроса и посмотреть самый дорогой вызов? Или хотя бы здесь выложить flame-graph с вызовами.
Что хоть тормозило, наносекунды? А зачем такая точность, особенно, если я понимаю, тормозит. У вас же там секунды (а не микросекунды) ожиданий.
slonopotamus
00.00.0000 00:00+1Timestamp у них растягивался, написано же.
Vest
00.00.0000 00:00+1Я читал тот участок, но не понял как растягивался. Вдоль или в ширину? А вы поняли?
schernolyas
00.00.0000 00:00Нда ... Просто Hotspot сгенерил нативную версию метода и выполнил ее быстрее. Поищите старый доклад Валерия Иванова из Oracle про динамическую компиляцию.
23derevo
00.00.0000 00:00+1Из статьи непонятно вообще ничего. Что вы оптимизировали, как, зачем, почему? Что там за требования к процессу такие, что «вместо» JMH вам пришлось тащить что-то ещё? И зачем вообще JMH, если вы какую-то энрепрайз штуку измеряете? Это же тул для _микро_бенчмарков.
PqDn
А содержание метода, который оптимизировали, большая тайна?
ris58h
У нас есть такие приборы!