Тест производительности не зависит от количества итераций

Попытка ответить на этот билет: В чем разница между instanceof и Class.isAssignableFrom(...)?

Я сделал тест производительности:

class A{}
class B extends A{}

A b = new B();

void execute(){
  boolean test = A.class.isAssignableFrom(b.getClass());
  // boolean test = A.class.isInstance(b);
  // boolean test = b instanceof A;
}

@Test
public void testPerf() {
  // Warmup the code
  for (int i = 0; i < 100; ++i)
    execute();

  // Time it
  int count = 100000;
  final long start = System.nanoTime();
  for(int i=0; i<count; i++){
     execute();
  }
  final long elapsed = System.nanoTime() - start;
System.out.println(count+" iterations took " + TimeUnit.NANOSECONDS.toMillis(elapsed) + "ms.);
}

Который дал мне:

  • A.class.isAssignableFrom(b.getClass()): 100000 итераций заняли 15 мс
  • A.class.isInstance(b): 100000 итераций заняли 12 мс
  • b instanceof A: 100000 итераций заняли 6 мс

Но, играя с количеством итераций, я вижу, что производительность постоянна. Для Integer.MAX_VALUE:

  • A.class.isAssignableFrom(b.getClass()): 2147483647 итерации заняли 15 мс
  • A.class.isInstance(b): 2147483647 итерации заняли 12 мс
  • b instanceof A: 2147483647 итерации заняли 6 мс

Думаю, что это была оптимизация компилятора (я провел этот тест с JUnit), я изменил его на это:

@Test
public void testPerf() {
    boolean test = false;

    // Warmup the code
    for (int i = 0; i < 100; ++i)
        test |= b instanceof A;

    // Time it
    int count = Integer.MAX_VALUE;
    final long start = System.nanoTime();
    for(int i=0; i<count; i++){
        test |= b instanceof A;
    }
    final long elapsed = System.nanoTime() - start;
    System.out.println(count+" iterations took " + TimeUnit.NANOSECONDS.toMillis(elapsed) + "ms. AVG= " + TimeUnit.NANOSECONDS.toMillis(elapsed/count));

    System.out.println(test);
}

Но производительность по-прежнему "независима" от количества итераций. Может ли кто-нибудь объяснить это поведение?

Ответ 1

JIT-компилятор может исключить циклы, которые ничего не делают. Это может быть активировано после 10 000 итераций.

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

Ответ 2

  • Сто итераций недостаточно для разминки. Порог компиляции по умолчанию - 10000 итераций (в сотни раз больше), поэтому лучше всего по крайней мере немного превысить этот порог.
  • Как только компиляция была запущена, мир не остановлен; компиляция выполняется в фоновом режиме. Это означает, что его эффект начнет наблюдаться только после небольшой задержки.
  • Существует достаточно места для оптимизации вашего теста таким образом, чтобы весь цикл сворачивался в конечный результат. Это объясняет постоянные числа.

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

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

Ответ 3

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