Замеряй и ускоряй: как мы сократили время вызова метода в Java-коде в 16 раз

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

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Привет, Хабр! Замер производительности кода — не самое простое упражнение для разработчика. Приходится решать кучу сложностей: разбираться с методом, создавать правильные условия. И всё равно можно получить результат с погрешностью, потому что любой метод «не бесплатный» и требует ресурсов процессора.

Меня зовут Александр Певненко, я 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;
}

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

Возникают вопросы:

  1. Получается, мы никак не избежим погрешности при вычислении «стоимости» метода work()

  2. Что считать baseline и на основе чего его строить? 

  3. Как «сжечь» время при замере? 

Но после подробного изучения метода оказалось: нет, погрешности избежать можно. Для этого есть несколько вариантов. Самый простой — использовать библиотеки JMH для Java. 

JMH: чем так хороши и почему не подошли нам

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

  • узнаём латентность — время на вызов System.nanoTime(),

  • измеряем гранулярность метода — разрешающую способность, минимальную ненулевую разницу между вызовами метода System.nanoTime()

Это позволит нам получить значение, которое будет коррелировать с длительностью исполнения метода взятия времени. Вроде бы всё просто: JMH сама вычисляет латентность и гранулярность. Но расслабляться всё равно не стоит. На разных ОС измерение времени с помощью латентности и гранулярности может быть разным, поэтому, вызывая этот метод при большом количестве потоков, нужно быть осторожным.

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

Тестирование оптимальности кода в «боевых условиях»

Ещё один способ избежать погрешности — измерить «стоимость» System.nanoTime(). Это позволит нам получить значение, которое будет коррелировать с длительностью исполнения метода взятия времени. 

Для решения проблемы с повторным вызовом метода я подготовил самописный бенчмарк и попробовал вычесть baseline до оптимизации и после. Затем сравнил, сколько времени требовалось на исполнение кода до и после доработок. Чтобы работать было проще, собрал графический интерфейс для визуализации на Python. Вот что получилось:

Набор данных до оптимизации.
Набор данных до оптимизации.
Визуализация набора данных. 
Визуализация набора данных. 

Аналогичные измерения после оптимизации давали видимый результат. На графике ниже — измерения до оптимизации (верхние линии) и после (нижние):

Визуальное сравнение двух наборов данных.
Визуальное сравнение двух наборов данных.
Визуальное сравнение трёх наборов данных после усреднения значения экспериментов.
Визуальное сравнение трёх наборов данных после усреднения значения экспериментов.
Аналитический вывод — выигрыш в производительности после оптимизации.
Аналитический вывод — выигрыш в производительности после оптимизации.

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

Выводы

Измерять производительность кода стоит хотя бы из любопытства, а лучше для того, чтобы повысить скорость продукта и упростить развёртывание. Бенчмарки — отличное решение для этого. В нашем случае самописные бенчмарки помогли серьёзно сократить длительность вызова System.nanoTime(). Сейчас работаем над тем, чтобы в большинстве проектов тестировать оптимальность всей системы ради снижения количества потенциальных проблем.

Писать собственные бенчмарки и устранять погрешности с помощью вычитания «стоимости» метода — удел не для каждого. Тем более, что в JMH все эти проблемы решаются автоматически, измерением латентности и гранулярности. Так что вполне можно пользоваться такими решениями: работы намного меньше, а польза очевидна.

Источник: https://habr.com/ru/company/sberbank/blog/726100/


Интересные статьи

Интересные статьи

Люди чаще всего интересуются погодой и временем — любым, включая время суток, время ухода с работы, продолжительность антракта в театре или режим работы государственного учреждения. Пог...
Если вы разрабатывает приложение, работающее по сети, или проводите отладку работы такого приложения, доскональное знание работы сетевых протоколов сильно облегчит вашу задачу. Первои...
Каждый разработчик делал пет-проект, который так и не увидел свет. У меня таких проектов набралась чёртова дюжина, а на этой неделе я был близок к тому, чтобы пополнить список неудач ...
Людям с таким психологическим портретом, как у меня, бесполезно заниматься тайм-менеджментом. Интересно очень многое, проектов одновременно несколько, мышление стратегическое, взаимодействие со...
Основанная в 1998 году компания «Битрикс» заявила о себе в 2001 году, запустив первый в России интернет-магазин программного обеспечения Softkey.ru.