Опция JVM для оптимизации операторов цикла - PullRequest
7 голосов
/ 18 февраля 2012

В школе мне говорили, что модифицировать индексную переменную for loop:

пример:

for(int i = 0 ; i < limit ; i++){
    if(something){
        i+=2;      //bad
    }
    if(something){
        limit+=2;      //bad
    }
}

- это плохая практика. Аргумент состоял в том, что некоторая оптимизация компилятора может оптимизировать цикл и не пересчитывать индекс и привязку в каждом цикле.

Я провел некоторый тест в java, и кажется, что по умолчанию индекс и граница пересчитываются каждый раз.

Мне интересно, возможно ли активировать этот тип функции в JVM HotSpot?

Например, для оптимизации такого родацикла:

for(int i = 0 ; i < foo.getLength() ; i++){   }

без необходимости писать:

int length = foo.getLength()
for(int i = 0 ; i < length ; i++){   }

Это просто пример, мне интересно попробовать и увидеть улучшения.

РЕДАКТИРОВАТЬ

Согласно ответу Питера Лоури , почему в этом простом примере JVM не использует метод inline getLenght()? :

public static void main(String[] args) {
   Too t = new Too();
   for(int j=0; j<t.getLength();j++){
   }
}


class Too {

    int l = 10;
    public Too() {
    }
    public int getLength(){
        //System.out.println("test");
        return l;
    }
}

На выходе«test» печатается 10 раз.

Я думаю, было бы неплохо оптимизировать этот тип выполнения.

EDIT 2: Кажется, я неправильно понял ...

Я удалил println, и действительно, профилировщик сказал мне, что метод getLength() в этом случае даже не вызывается один раз.

Ответы [ 4 ]

13 голосов
/ 18 февраля 2012

Это зависит от того, что делает foo.getLength (). Если это может быть встроено, это может быть фактически то же самое. Если это не может быть встроено, JVM не может определить, является ли результат тем же.

Кстати, вы можете написать для одного лайнера.

for(int i = 0, length = foo.getLength(); i < length; i++){   }

РЕДАКТИРОВАТЬ: Это ничего не стоит;

  • методы и циклы обычно не оптимизируются до тех пор, пока они не будут вызваны 10000 раз.
  • вызовы подвыборки профилировщиков для сокращения накладных расходов. Они могут рассчитывать каждые 10 или 100 или более, поэтому тривиальный пример может не отображаться.
12 голосов
/ 18 февраля 2012

Я провел некоторый тест в Java, и кажется, что по умолчанию индекс и граница каждый раз пересчитываются.

В соответствии со спецификацией языка Java это:

for(int i = 0 ; i < foo.getLength() ; i++){   }

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

Так что это то, что компилятор разрешено делать.

Интересно, возможно ли активировать такую ​​функцию в JVM HotSpot?

Простой ответ* Нет. 1028 *

Вы, кажется, предлагаете переключатель компилятора, который сообщает / позволяет компилятору игнорировать правила JLS.Там нет такого переключателя.Такой переключатель будет ПЛОХАЯ ИДЕЯ .Это может привести к поломке правильных / действующих / работающих программ.Учтите это:

class Test {
   int count;

   int test(String[] arg) {
       for (int i = 0; i < getLength(arg); i++) {
           // ...
       }
       return count;
   }

   int getLength(String[] arg) {
       count++;
       return arg.length;
   }
}

Если бы компилятору было разрешено перемещать вызов getLength(arg) из цикла, это изменило бы количество раз, которое вызывался метод, и, следовательно, изменило значение, возвращаемоеtest метод.

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


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

4 голосов
/ 18 февраля 2012

Основная причина этого не в том, что это усложняет понимание и поддержку кода.

Независимо от того, что оптимизирует JVM, это не повлияет на правильность программы. Если он не может выполнить оптимизацию, поскольку индекс изменяется внутри цикла, он не будет оптимизировать его. Я не вижу, как тест Java может показать, есть или нет такая оптимизация.

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

2 голосов
/ 18 февраля 2012

Прежде чем мы углубимся в рассуждения , почему доступ к полю не встроен.Может быть, нам следует показать, что да, если вы знаете, что ищете (что на самом деле нетривиально в Java), доступ к полю встроен просто отлично.

Сначала нам нужно понять основы работы JIT - и я действительно не могу сделать это в одном ответе.Достаточно сказать, что JIT работает только после того, как функция вызывается достаточно часто (обычно> 10 тыс.)

Поэтому мы используем следующий код для фактического тестирования:

public class Test {
    private int length;

    public Test() {
        length = 10000;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 14000; i++) {
            foo();
        }
    }

    public static void foo() {
        Test bar = new Test();
        int sum = 0;
        for (int i = 0; i < bar.getLength(); i++) {
            sum += i;
        }
        System.out.println(sum);
    }

    public int getLength() {
        System.out.print("_");
        return length;
    }    
}

Теперь мы компилируемэтот код и запустите его с java.exe -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*Test.foo Test >Test.txt, что приводит к нечестивому длинному выводу, но интересная часть:

  0x023de0e7: mov    %esi,0x24(%esp)
  0x023de0eb: mov    %edi,0x28(%esp)
  0x023de0ef: mov    $0x38fba220,%edx   ;   {oop(a 'java/lang/Class' = 'java/lang/System')}
  0x023de0f4: mov    0x6c(%edx),%ecx    ;*getstatic out
                                        ; - Test::getLength@0 (line 24)
                                        ; - Test::foo@14 (line 17)
  0x023de0f7: cmp    (%ecx),%eax        ;*invokevirtual print
                                        ; - Test::getLength@5 (line 24)
                                        ; - Test::foo@14 (line 17)
                                        ; implicit exception: dispatches to 0x023de29b
  0x023de0f9: mov    $0x3900e9d0,%edx   ;*invokespecial write
                                        ; - java.io.PrintStream::print@9
                                        ; - Test::getLength@5 (line 24)
                                        ; - Test::foo@14 (line 17)
                                        ;   {oop("_")}
  0x023de0fe: nop    
  0x023de0ff: call   0x0238d1c0         ; OopMap{[32]=Oop off=132}
                                        ;*invokespecial write
                                        ; - java.io.PrintStream::print@9
                                        ; - Test::getLength@5 (line 24)
                                        ; - Test::foo@14 (line 17)
                                        ;   {optimized virtual_call}
  0x023de104: mov    0x20(%esp),%eax
  0x023de108: mov    0x8(%eax),%ecx     ;*getfield length
                                        ; - Test::getLength@9 (line 25)
                                        ; - Test::foo@14 (line 17)
  0x023de10b: mov    0x24(%esp),%esi
  0x023de10f: cmp    %ecx,%esi
  0x023de111: jl     0x023de0d8         ;*if_icmpge
                                        ; - Test::foo@17 (line 17)

, который является внутренним циклом, который мы фактически выполняем.Обратите внимание, что следующий 0x023de108: mov 0x8(%eax),%ecx загружает значение длины в регистр - все, что указано выше, предназначено для вызова System.out (я бы удалил его, так как это усложняет его, но так как несколько человек думали, что это будет мешатьвстраивая я оставил это там).Даже если вы не вписываетесь в сборку x86, вы можете ясно увидеть: нигде нет инструкции вызова, кроме встроенного вызова записи.

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