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

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

12 голосов
/ 19 января 2012

Я играю с некоторым фрагментом кода, вычисляя время, необходимое для вычисления некоторого кода 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;
}

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

Ответы [ 6 ]

3 голосов
/ 19 января 2012

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

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

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

2 голосов
/ 20 января 2012

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

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

  1. http://code.google.com/p/caliper/
  2. Сравнительный анализ Java Matrix Benchmark

    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
1 голос
/ 20 января 2012

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

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

1 голос
/ 20 января 2012

Когда компилятор времени выполнения может выяснить, 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 в тесте.

1 голос
/ 19 января 2012

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

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

Кстати, в последней версии Eclipse, как это всегда было в Windows, запуск этого кода через IDE «Выполнить» (без отладки) в 10 раз медленнее, чем запуск из консоли, так что об этом ...

0 голосов
/ 19 января 2012

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

   if (testFunc(3))
     counter++;

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...