Как убедиться в отсутствии оптимизации jvm и компилятора

У меня есть этот код, который тестирует Calendar.getInstance().getTimeInMillis() vs System.currentTimeMilli():

long before = getTimeInMilli();
for (int i = 0; i < TIMES_TO_ITERATE; i++)
{
  long before1 = getTimeInMilli();
  doSomeReallyHardWork();
  long after1 = getTimeInMilli();
}
long after = getTimeInMilli();
System.out.println(getClass().getSimpleName() + " total is " + (after - before));

Я хочу убедиться, что оптимизация JVM или компилятора не выполняется, поэтому тест будет действительным и на самом деле будет показывать разницу.

Как быть уверенным?

EDIT: я изменил пример кода, чтобы он стал более понятным. Здесь я проверяю, сколько времени требуется для вызова getTimeInMilli() в разных реализациях - Calendar vs System.

Ответ 1

Я думаю, вам нужно отключить JIT. Добавьте в свою следующую команду:

-Djava.compiler=NONE

Ответ 2

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

Однако, если вы хотите удостовериться, что JVM не удаляет вызовы, которые в противном случае он мог бы рассматривать без-ops, один из вариантов - использовать результат - поэтому, если вы вызываете System.currentTimeMillis() несколько раз, вы можете суммировать все возвращаемые значения, а затем отображать сумму в конце.

Обратите внимание, что вы все еще можете иметь некоторое предвзятое отношение - например, может быть какая-то оптимизация, если JVM может дешево определить, что прошло с момента последнего вызова System.currentTimeMillis() только крошечное количество времени, поэтому он может использовать кешированное значение. Я не говорю, что на самом деле это дело, но это то, о чем вам нужно подумать. В конечном счете, тесты могут только реально протестировать нагрузки, которые вы им даете.

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

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

Ответ 3

Извините, но то, что вы пытаетесь сделать, мало смысла.

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

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

Лучшим подходом было бы сделать это:

long before1 = getTimeInMilli();
for (int i = 0; i < TIMES_TO_ITERATE; i++) {
    doSomeReallyHardWork();
}
long after1 = getTimeInMilli();

... и/или использовать наносекундные часы.


Если вы пытаетесь измерить время, затраченное на вызов двух версий getTimeInMillis(), тогда я не понимаю точку вашего вызова doSomeReallyHardWork(). Более приемлемым эталоном будет следующее:

public long test() {
    long before1 = getTimeInMilli();
    long sum = 0;
    for (int i = 0; i < TIMES_TO_ITERATE; i++) {
        sum += getTimeInMilli();
    }
    long after1 = getTimeInMilli();
    System.out.println("Took " + (after - before) + " milliseconds");
    return sum;
}

... и вызывайте это несколько раз, пока не будет стабилизировано время.

В любом случае, мой основной момент все еще стоит, превращение JIT-компиляции и/или оптимизации означало бы, что вы измеряете то, что не полезно знать, а не то, что вы действительно пытаетесь выяснить. (Если, конечно, вы намереваетесь запускать свое приложение в процессе производства с отключенным JIT... что мне трудно поверить...)

Ответ 4

То, что вы делаете, похоже на бенчмаркинг, вы можете прочитать "Надежный сравнительный анализ Java" , чтобы получить хороший справочник о том, как сделать все правильно. В нескольких словах вам не нужно отключать его, потому что это будет не то, что происходит на производственном сервере. Вместо этого вам нужно знать, как закрыть возможную "реальную" оценку времени/производительность. Перед оптимизацией вам нужно "разогреть" ваш код, он выглядит так:

// warm up
for (int j = 0; j < 1000; j++) {
    for (int i = 0; i < TIMES_TO_ITERATE; i++)
    {
        long before1 = getTimeInMilli();
        doSomeReallyHardWork();
        long after1 = getTimeInMilli();
    }
}

// measure time
long before = getTimeInMilli();
for (int j = 0; j < 1000; j++) {
    for (int i = 0; i < TIMES_TO_ITERATE; i++)
    {
        long before1 = getTimeInMilli();
        doSomeReallyHardWork();
        long after1 = getTimeInMilli();
    }
}
long after = getTimeInMilli();

System.out.prinltn( "What to expect? " + (after - before)/1000 ); // average time

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

public void doIt() {
    for (int i = 0; i < TIMES_TO_ITERATE; i++)
    {
        long before1 = getTimeInMilli();
        doSomeReallyHardWork();
        long after1 = getTimeInMilli();
    }
}

// warm up
for (int j = 0; j < 1000; j++) {
    doIt()
}

// measure time
long before = getTimeInMilli();
for (int j = 0; j < 1000; j++) {
    doIt();
}
long after = getTimeInMilli();

System.out.prinltn( "What to expect? " + (after - before)/1000 ); // average time

Второй подход более точен, но он также зависит от ВМ. Например. HotSpot может выполнять "on-stack replacement" , это означает, что если какая-то часть метода выполняется очень часто, она будет оптимизирована VM и старой версией кода будет обмениваться с оптимизированным во время выполнения метода. Конечно, это требует дополнительных действий со стороны VM. JRockit этого не делает, оптимизированная версия кода будет использоваться только тогда, когда этот метод будет выполнен снова (так что оптимизация "runtime"... Я имею в виду, что в моем первом примере кода все время будет выполняться старый код... кроме doSomeReallyHardWork внутренности - они не принадлежат этому методу, поэтому оптимизация будет работать хорошо).

ОБНОВЛЕНО: код, о котором идет речь, был отредактирован, когда я отвечал;)