Рубрики
Без рубрики

Производительность Java – 2 – Подход к тестированию производительности

Резюме Эта статья является частью 3 для серии Java Performance, в которой обобщается java… Помечено книгами, java, программированием, производительностью.

Эта статья является частью 3 серии Производительность Java которые обобщают книгу о производительности Java по Скотт Оукс

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

В этой главе мы обсудим некоторые интересные записи о производительности. Мы поймем разницу между:

  • Микропленки
  • Макроэкономические ориентиры
  • Я так думаю, что мы также поговорим о времени отклика, пакетной обработке и пропускной способности, поймем изменчивость и увидим несколько интересных примеров кода.

Отлично, давайте начнем вторую главу…

Подход к тестированию производительности

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

A- Микропленки

Первой из этих категорий является микробенчмарка. Микробенчмарка – это тест, предназначенный для измерения очень маленькой единицы производительности, например:

  • Время вызова синхронизированного метода по сравнению с несинхронизированным методом
  • Накладные расходы при создании потока по сравнению с использованием пула потоков
  • Время выполнения одного арифметического алгоритма по сравнению с альтернативной реализацией

Моменты, о которых следует позаботиться Рассмотрим следующий код, который измеряет производительность различных реализаций метода для вычисления 50-го числа Фибоначчи:

public void doTest() { // Main Loop
    double l;
    long then = System.currentTimeMillis();
    for(inti=0;i 0"); if (n == 0) return 0d;
    if (n == 1) return 1d;
    double d = fibImpl1(n - 2) + fibImpl(n - 1);
    if (Double.isInfinite(d)) throw new ArithmeticException("Overflow");
    return d; 
}

В предыдущем коде были следующие проблемы: 1- Микробенчмарки должны использовать свои результаты : Самая большая проблема с этим кодом заключается в том, что он фактически никогда не изменяет состояние программы. Поскольку результат вычисления Фибоначчи никогда не используется, компилятор может отказаться от этого вычисления. Умный компилятор завершит работу следующим кодом:

long then = System.currentTimeMillis();
long now = System.currentTimeMillis(); System.out.println("Elapsed time: " + (now - then));

Почему? : поскольку вы никогда не использовали переменную l в своем коде, компилятор предположит, что это избыточный код, удалив его перед выполнением. Решение попробуйте использовать переменную l любым удобным вам методом.

...
consume(l);
...

2- Микробенчмарки не должны содержать посторонних операций : Этот код выполняет только одну операцию: вычисление 50-го числа Фибоначчи. Очень умный компилятор может понять это и выполнить цикл только один раз — или, по крайней мере, отказаться от некоторых итераций цикла, поскольку эти операции являются избыточными. Кроме того, производительность fibImpl(1000), вероятно, будет сильно отличаться от производительности fibImpl(1); если цель состоит в том, чтобы сравнить производительность различных реализаций, то необходимо учитывать диапазон входных значений. Самый простой способ закодировать использование генератора случайных чисел – обработать цикл следующим образом:

int[] input = new int[nLoops]; 
for(inti=0;i

3- Микрометки должны измерять правильный ввод : Третья ошибка здесь – диапазон входных данных теста: выбор произвольных случайных значений не обязательно отражает то, как будет использоваться код. В этом случае исключение будет немедленно выдано на половине вызовов тестируемого метода (все, что имеет отрицательное значение). Исключение также будет создаваться всякий раз, когда входной параметр превышает 1476, поскольку это наибольшее число Фибоначчи, которое может быть представлено в двойном виде. Рассмотрим эту альтернативную реализацию:

public double fibImplSlow(int n) {
    if (n < 0) throw new IllegalArgumentException("Must be > 0"); 
    if (n > 1476) throw new ArithmeticException("Must be < 1476"); 
    return verySlowImpl(n);
}

Если эта реализация очень медленная “медленнее, чем первая в пункте № 2”, это даст лучшие результаты производительности, потому что в ней проверяется диапазон ввода строки 1 и 2 .

4- Нет периода прогрева : Предыдущая реализация не предлагает период прогрева , что важно, потому что одной из характеристик производительности Java является то, что код работает лучше, чем больше он выполняется, тема, которая рассматривается в Глава 4 , что-то связанное с компиляторами JIT в java. Таким образом, окончательная версия теста производительности должна быть следующей:

package net.sdo;
import java.util.Random;
public class FibonacciTest { 
    private volatile double l; private int nLoops; 
    private int[] input;
    public static void main(String[] args) {
        FibonacciTest ft = new                 
        FibonacciTest(Integer.parseInt(args[0])); 
        ft.doTest(true);
        ft.doTest(false);
    }

    private FibonacciTest(int n) { 
        nLoops = n;
        input = new int[nLoops];
        Random r = new Random(); 
        for(inti=0;i 0");
        if (n == 0) return 0d;
        if (n == 1) return 1d;
        double d = fibImpl1(n - 2) + fibImpl(n - 1);
        if (Double.isInfinite(d)) throw new ArithmeticException("Overflow"); 
        return d;
    } 
}

B- Макробенчмарки

Лучшее, что можно использовать для измерения производительности приложения, – это само приложение в сочетании с любыми внешними ресурсами, которые оно использует. Тестирование всего приложения со всеми внешними ресурсами называется Макро-бенчмарк . Сложные системы – это нечто большее, чем сумма их частей; они будут вести себя совершенно по-другому, когда эти части будут собраны. Например, имитация вызовов базы данных может означать, что вам больше не нужно беспокоиться о производительности базы данных ‐ и, эй, вы специалист по Java; почему вы должны иметь дело с проблемой производительности кого—то другого? Другая причина для тестирования полного приложения заключается в распределении ресурсов. В идеальном мире было бы достаточно времени для оптимизации каждой строки кода в приложении. В реальном мире сроки приближаются, и оптимизация только одной части сложной среды может не принести немедленной выгоды.

C- Мне так жаль

Инженеры Java EE склонны использовать этот термин применительно к чему‐то другому: контрольным показателям, которые измеряют один аспект производительности, но все равно выполняют много кода. Примером Java EE может быть то, что измеряет, насколько быстро ответ от простого JSP может быть возвращен с сервера приложений. Код, участвующий в таком запросе, является существенным по сравнению с традиционной микробенчмаркой: существует много кода управления сокетами, кода для чтения запроса, кода для поиска (и, возможно, компиляции) JSP, кода для написания ответа и так далее. С традиционной точки зрения, это не микробенчмаркинг. Этот вид теста также не является макро-тестом: нет безопасности (например, пользователь не входит в приложение), нет управления сеансами и нет использования множества других функций Java EE. Так что это называется эталоном Me so.

Общие примеры Кода

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

  • Основным объектом в приложении является объект Цены акций , который представляет диапазон цен на акции в данный день:
public interface StockPrice { 
    String getSymbol();
    Date getDate();
    BigDecimal getClosingPrice();
    BigDecimal getHigh();
    BigDecimal getLow();
    BigDecimal getOpeningPrice();
    boolean isYearHigh();
    boolean isYearLow();
    Collection getOptions();
}

Примеры приложений обычно содержат набор этих цен, представляющих историю акций за определенный период времени (например, 1 год или 25 лет, в зависимости от примера).:

public interface StockPriceHistory { 
    StockPrice getPrice(Date d);
    Collection getPrices(Date startDate, Date endDate);
    Map getAllEntries();
    Map> getHistogram();
    BigDecimal getAveragePrice();
    Date getFirstDate();
    BigDecimal getHighPrice();
    Date getLastDate();
    BigDecimal getLowPrice();
    BigDecimal getStdDev();
    String getSymbol();
}

Базовая реализация этого класса загружает набор цен из базы данных:

public class StockPriceHistoryImpl implements StockPriceHistory { 
    ...
    public StockPriceHistoryImpl(String s, Date startDate, Date endDate, EntityManager em) {
        Date curDate = new Date(startDate.getTime()); 
        symbol = s;
        while (!curDate.after(endDate)) {
            StockPriceImpl sp = em.find(StockPriceImpl.class, new StockPricePK(s, (Date) curDate.clone())); 
            if (sp != null) {
                Date d = (Date) curDate.clone(); 
                if (firstDate == null) {
                    firstDate = d;
                }
                prices.put(d, sp);
                lastDate = d;
            }
            curDate.setTime(curDate.getTime() + msPerDay);
        }
    }
    ... 
}

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

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

A – Измерения Затраченного времени (Пакетные):

Самый простой способ измерения производительности – посмотреть сколько времени требуется для выполнения определенной задачи , пример:

  • Извлеките историю 10 000 акций за 25-летний период и рассчитайте стандартное отклонение этих цен; подготовьте отчет о выплатах по заработной плате для 50 000 сотрудников корпорации; выполните цикл 1 000 000 раз.

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

B- Измерения пропускной способности:

Измерение пропускной способности основано на объеме работы, который может быть выполнен за определенный период времени . Записи:

  • В тестировании клиент-сервер измерение пропускной способности означает, что у клиентов нет времени на раздумья. Если есть один клиент, этот клиент отправляет запрос на сервер. Когда он получает ответ, он немедленно отправляет новый запрос.
  • Это измерение часто называют транзакциями в секунду ( TPS ), запросами в секунду ( RPS ) или операциями в секунду ( OPS ).
  • Все тесты клиент-сервер сопряжены с риском того, что клиент не сможет достаточно быстро отправить данные на сервер. Это может произойти из-за того, что на клиентской машине недостаточно циклов процессора для запуска нужного количества клиентских потоков, или из-за того, что клиенту приходится тратить много времени на обработку запроса, прежде чем он сможет отправить новый запрос. В этих случаях тест эффективно измеряет производительность клиента, а не производительность сервера, что обычно не является целью.
  • Обычно тесты, измеряющие пропускную способность, также сообщают о среднем времени отклика на свои запросы.

C- Тесты времени отклика

Последним распространенным тестом является тот, который измеряет время отклика: количество времени, которое проходит между отправкой запроса от клиента и получением ответа . Разница между временем отклика и пропускной способностью заключается в том, что клиентские потоки находятся в режиме ожидания теста времени отклика в течение некоторого периода времени между операциями. Это называется время подумать . Когда в тест вводится время на обдумывание, пропускная способность становится фиксированной: заданное количество клиентов, выполняющих запросы с заданным временем обдумывания, всегда будет давать один и тот же TPS. пример : Самый простой способ для клиентов – спать в течение некоторого периода времени между запросами:

while (!done) {
    time = executeOperation();                 
    Thread.currentThread().sleep(30*1000);
}

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

while (!done) {
    time = executeOperation(); 
    Thread.currentThread().sleep(30*1000 - time);
}

Эта альтернатива обеспечивает фиксированную пропускную способность 0,033 операций на клиента независимо от времени ответа (при условии, что в этом примере время отклика всегда меньше 30 секунд).

Существует два способа измерения времени отклика . Время отклика может быть указано как:

  • Среднее : отдельные времена суммируются и делятся на количество запросов.
  • Процентильный запрос : например, время отклика 90%. Если 90% ответов длятся менее 1,5 секунд, а 10% ответов длятся более 1,5 секунд, то 1,5 секунды – это 90-е% времени отклика. Следующие 2 графика показывают, насколько важен процентиль:

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

  • Фоновые процессы на компьютере повлияют на приложение
  • при запуске программы сеть будет более или менее перегружена
  • … и т.д.

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

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

  • Разработчики ограничены во времени, чтобы проверить код в соответствии с графиком; они будут возражать против необходимости тратить время на устранение проблемы с производительностью, когда в расписании есть время для этого после того, как весь исходный код будет проверен. Разработчик, который проверяет код, вызывающий регрессию на 1% в начале цикла, столкнется с необходимостью устранить эту проблему; разработчик, который ждет до вечера замораживания функции, может проверить код, вызывающий регрессию на 20%, и разобраться с ним позже.
  • Характеристики производительности кода будут меняться по мере изменения кода. Это тот же принцип, который использовался для тестирования полного приложения (в дополнение к любым тестам на уровне модуля, которые могут возникнуть): использование кучи изменится, компиляция кода изменится и так далее.
  • Разработчик, который вводит код, вызывающий 5%-ную регрессию, может иметь план устранения этой регрессии по мере продолжения разработки: возможно, ее код зависит от какой-то еще не интегрированной функции, и когда эта функция будет доступна, небольшая настройка позволит регрессии исчезнуть. Это разумная позиция, даже если это означает, что тестам производительности придется смириться с этой 5%-ной регрессией в течение нескольких недель (и прискорбной, но неизбежной проблемой, заключающейся в том, что указанная регрессия маскирует другие регрессии).

Раннее, частое тестирование наиболее полезно, если соблюдаются следующие рекомендации:

А- Автоматизировать все

  • Все тесты производительности должны быть написаны по сценарию
  • Сценарии должны иметь возможность запускать тест несколько раз
  • Выполните анализ результатов t-теста
  • Подготовьте отчет, показывающий уровень уверенности в том, что результаты будут одинаковыми
  • Автоматизация должна убедиться, что машина находится в известном состоянии, прежде чем запускать тесты

Б- Измерять все

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

C- Запуск в целевой системе

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

🏃 Увидимся в главе 3 …

Занимайтесь спортом, чтобы эффективно работать своим мозгом, тренируйте свое тело. 🏊

Оригинал: “https://dev.to/yousef_zook/java-performance-chapter-2-1m10”