Подтвердить что ты не робот

Эффективность Java

Я играю с некоторым фрагментом кода, вычисляя время, необходимое для вычисления некоторого кода Java, чтобы получить представление об эффективности или неэффективности некоторых функций Java. Поэтому я застрял сейчас с каким-то действительно странным эффектом, который я просто не могу объяснить. Может быть, кто-то из вас может помочь мне понять это.

public class PerformanceCheck {

 public static void main(String[] args) {
    List<PerformanceCheck> removeList = new LinkedList<PerformanceCheck>();

    int maxTimes = 1000000000;

    for (int i=0;i<10;i++) {
        long time = System.currentTimeMillis();

        for (int times=0;times<maxTimes;times++) {
            // PERFORMANCE CHECK BLOCK START

            if (removeList.size() > 0) {
                testFunc(3);
            }

            // PERFORMANCE CHECK BLOCK END
        }

        long timeNow = System.currentTimeMillis();
        System.out.println("time: " + (timeNow - time));
    }
 }

 private static boolean testFunc(int test) {
    return 5 > test;
 }

}

Запуск этого результата в относительно длительное время вычисления (помните, что removeList пуст, поэтому testFunc даже не вызывается):

time: 2328
time: 2223
...

При замене чего-либо из комбинации removeList.size() > 0 и testFunc (3) с чем-либо еще имеет лучшие результаты. Например:

...
if (removeList.size() == 0) {
    testFunc(3);
}
...

Результаты в (testFunc вызывается каждый раз):

time: 8
time: 7
time: 0
time: 0

Даже вызов обеих функций, независимых друг от друга, приводит к более низкому времени вычисления:

...
if (removeList.size() == 0);
    testFunc(3);
...

Результат:

time: 6
time: 5
time: 0
time: 0
...

Только эта конкретная комбинация в моем первоначальном примере занимает так много времени. Это раздражает меня, и я бы очень хотел это понять. Что в этом особенного?

Спасибо.

Дополнение:

Изменение testFunc() в первом примере

if (removeList.size() > 0) {
                testFunc(times);
}

для чего-то еще, например

private static int testFunc2(int test) {
    return 5*test;
}

Результат будет быстрым снова.

4b9b3361

Ответ 1

Это действительно удивительно. Сгенерированный байт-код идентичен, за исключением условного, который равен ifle vs ifne.

Результаты намного более разумны, если вы отключите JIT с помощью -Xint. Вторая версия в 2 раза медленнее. Так что это связано с оптимизацией JIT.

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

Ответ 2

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

Есть также много нюансов при сравнении Java-кода. Я писал о некоторых проблемах, с которыми я столкнулся в Java Matrix Benchmark, например, как прошлая история может влиять на текущие результаты. Вы избежите многих из этих проблем, используя суппорт.

  • http://code.google.com/p/caliper/
  • Проблемы с бенчмаркингом с тест-матрицей Java

    public class PerformanceCheck extends SimpleBenchmark {
    
    public int timeFirstCase(int reps) {
        List<PerformanceCheck> removeList = new LinkedList<PerformanceCheck>();
        removeList.add( new PerformanceCheck());
        int ret = 0;
    
        for( int i = 0; i < reps; i++ )  {
            if (removeList.size() > 0) {
                if( testFunc(i) )
                    ret++;
            }
        }
    
        return ret;
    }
    
    public int timeSecondCase(int reps) {
        List<PerformanceCheck> removeList = new LinkedList<PerformanceCheck>();
        removeList.add( new PerformanceCheck());
        int ret = 0;
    
        for( int i = 0; i < reps; i++ )  {
            if (removeList.size() == 0) {
                if( testFunc(i) )
                    ret++;
            }
        }
    
        return ret;
    }
    
    private static boolean testFunc(int test) {
        return 5 > test;
    }
    
    public static void main(String[] args) {
        Runner.main(PerformanceCheck.class, args);
    }
    }
    

ВЫВОД:

 0% Scenario{vm=java, trial=0, benchmark=FirstCase} 0.60 ns; σ=0.00 ns @ 3 trials
50% Scenario{vm=java, trial=0, benchmark=SecondCase} 1.92 ns; σ=0.22 ns @ 10 trials

 benchmark    ns linear runtime
 FirstCase 0.598 =========
SecondCase 1.925 ==============================

vm: java
trial: 0

Ответ 3

Хорошо, я рад, что не должен заниматься оптимизацией производительности Java. Я попробовал это самостоятельно с Java JDK 7 64-бит. Результаты произвольны;). Не имеет значения, какие списки я использую, или если я кэширую результат size() перед входом в цикл. Также полностью уничтожение тестовой функции практически не имеет разницы (так что она не может быть использована для предсказания ветвления). Флаги оптимизации повышают производительность, но являются произвольными.

Единственным логическим следствием здесь является то, что компилятор JIT иногда может оптимизировать утверждение (что не так сложно, чтобы быть правдой), но оно кажется довольно произвольным. Одна из многих причин, почему я предпочитаю такие языки, как С++, где поведение по крайней мере детерминировано, даже если оно иногда произвольно.

BTW в последнем Eclipse, как и всегда в Windows, запуск этого кода с помощью IDE "Запуск" (без отладки) в 10 раз медленнее, чем запуск с консоли, так много об этом...

Ответ 4

Когда компилятор времени выполнения может выяснить, что testFunc оценивает константу, я считаю, что он не оценивает цикл, что объясняет ускорение.

Когда условие removeList.size() == 0, функция testFunc(3) оценивается константой. Когда условие removeList.size() != 0, внутренний код никогда не оценивается, поэтому его нельзя ускорить. Вы можете изменить свой код следующим образом:

for (int times = 0; times < maxTimes; times++) {
            testFunc();  // Removing this call makes the code slow again!
            if (removeList.size() != 0) {
                testFunc();
            }
        }

private static boolean testFunc() {
    return testFunc(3);
}

Когда testFunc() изначально не вызван, компилятор времени выполнения не понимает, что testFunc() оценивает константу, поэтому он не может оптимизировать цикл.

Некоторые функции, такие как

private static int testFunc2(int test) {
    return 5*test;
}

компилятор, вероятно, пытается предварительно оптимизировать (перед выполнением), но, видимо, не для случая параметра передается как целое число и вычисляется в условном выражении.

Ваш тест возвращает время, например

time: 107
time: 106
time: 0
time: 0
...

предполагая, что для завершения компилятора во время выполнения требуется 2 итерации внешнего цикла для завершения оптимизации. Компиляция с флагом -server, вероятно, вернет все 0 в эталонном тесте.

Ответ 5

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

Если вы измените тест, чтобы сделать что-то незначительно полезным, разница исчезнет.

Ответ 6

Эти тесты жесткие, поскольку компиляторы настолько прокляты умными. Одна догадка: поскольку результат testFunc() игнорируется, компилятор может полностью оптимизировать его. Добавьте счетчик, что-то вроде

   if (testFunc(3))
     counter++;

И, просто для тщательности, сделайте System.out.println(counter) в конце.