16 Апрель, 2010

ldt или нагрузочное тестирование по-простому

И снова про инструменты разработки. Часто бывает необходимо сравнить производительность/пропускную способность того или иного участка кода, а писать тестирующий код ой как не хочется. А ведь надо всего-то, запустить нужный метод N раз и померять время выполнения.

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

Недолго думая, пишем простой POJO класс описывающий тестовый случай.

package com.blogspot.dotsid.ldt;

public class ArrayIterationTest {

  private int size;
  private int[] data;

  public void setSize(int size) {
    this.size = size;
  }

  public void prepare() {
    data = new int[size];
  }

  public void doTest() {
    for ( int i : data );
  }
}

компилируем исходник и находясь в classpath'е выполняем:

bazhenov@home ldt -z com.blogspot.dotsid.ArrayIterationTest#doTest -n 100 -p "size=1000000"
                     RESULTS
--------------------------------------------------
 Concurrency level             : 1
 Samples count (per thread)    : 100
 Total time                    : 180ms
 Min. time                     : 1ms
 Max. time                     : 5ms
 Throughput                    : 553 tps

В этом тесте мы создали массив размером 1 миллион позиций и проитерировались по нему 100 раз. Как видно мой процессор по этому массиву пробегает со скоростью несколько миллисекунд на одну полную итерацию.

Довольно простой и эффективный инструмент для выполнения нагрузочных тестов на отдельные модули системы. Основные возможности:

  • поддержка многопоточного тестирования;
  • поддержка warm up периода (указанное число первых прогонов теста может не участвовать в измерениях. Это бывает необходимо для обеспечения hot code execution path);
  • поддержка фикстур (prepare/cleanup);
  • sub millisecond accuracy;
  • поддержка maven;
Утилита open source и доступна на github. Еще один маленький и незатейливый инструмент, который полезно иметь под рукой. Ведь именно из таких инструментов и формируется окружение которое позволяет нам работать эффективно. Как говорится, "что нельзя измерить, тем нельзя управлять".


8 comments:

  1. А не правильнее ли было бы измерять тики (или что там ещё можно измерить), а не мс?

    ОтветитьУдалить
  2. Тут надо уточнить. Способ измерения основан как раз на tick'ах, просто потому что системный таймер не предоставляет возможности измерять время с точностью большей чем десятки миллисекунд (зависит от железа), а этого иногда недостаточно. Поэтому, для измерений используются tick'и, но результат для удобства интерпретации представляется в микро/миллисекундах.

    ОтветитьУдалить
  3. а как же nanoTime, но больше удивляет конструкция
    for ( int i : data );
    как правило её jit оптимизирует в nop - и т.о. замер nop-а это точный 0. с другой стороны гонять java приложение без jit'а очень сомнительная задача

    ОтветитьУдалить
  4. Ну на самом деле я nanoTime'ом и пользуюсь. System.nanoTime() использует как раз выше описанный механизм замера времени и предназначен как раз для получения elapsed time (наверное, стоило об этом сразу упомянуть).

    Конструкция for ( int i : data ); взята как тестовая. Я не уверен оптимизирует ли JIT эту конструкцию в NOP. По идее это может сделать и сам компилятор java, но он этого явно не делает:

    public void doTest();
    Code:
    0: aload_0
    1: getfield #3; //Field data:[I
    4: astore_1
    5: aload_1
    6: arraylength
    7: istore_2
    8: iconst_0
    9: istore_3
    10: iload_3
    11: iload_2
    12: if_icmpge 26
    15: aload_1
    16: iload_3
    17: iaload
    18: istore 4
    20: iinc 3, 1
    23: goto 10
    26: return

    К тому же NOP, насколько мне известно, не может иметь нулевой замер, так как процессор все же "исполняет" эту инструкцию. То есть выполняет fetch и decode.

    Видимо мне стоило подобрать менее синтетический пример для демонстрации :)

    ОтветитьУдалить
  5. как показало вскрытие (с) - этот цикл не был выкинут jit'ом (что на мой взгляд странно). а вот цикл вида

    for(int i = 0; i < 1000000; i++){int j = 2 * i;}

    выкидывает без зазрения совести

    ОтветитьУдалить
  6. Видимо JIT не eliminate'ит блоки кода в которых встречаются reference'ы на non local vaiables. Вне зависимости от того как они используются.

    Честно сказать, меня это не удивляет. Никто толком не знает как именно он работает :) JIT для меня это best effort. То есть писать код с оглядкой на то, что JIT соптимизирует конкретные use case'ы это, на мой взгляд, не лучшая идея.

    ОтветитьУдалить
  7. а вот писать performance test'ы лучше всего как раз с этой оглядкой

    ОтветитьУдалить
  8. Если вы имеете ввиду code elimination, то согласен. Если runtime удалит код, то тестировать нечего, а хороший тест должен что-то тестировать. Другое дело, что я так и не смог найти внятного описания техник противостояния code elimination.

    В "Java Concurrency in Practice" Brian'а Goetz'а есть отдельная глава посвященная performance тестированию приложений. И толку от нее мало. Я подозреваю, что большинству программистов все же неизвестно как работает JIT (я уже не говорю про влияние gc на производительность и то что поведение jit меняется от версии к версии).

    В большинстве случаев приходится ограничиваться наблюдениями ("что-то тест подозрительно быстро выполняется, надо смотреть disassembly").

    ОтветитьУдалить