Что такое метод инверсии петли?

89

Я просматривал документ, в котором говорится о методах оптимизации JIT -компилятора для Java. Одним из них была «инверсия петли». И в документе говорится:

Вы заменяете обычную whileпетлю do-whileпетлей. И do-whileцикл задается внутри ifпредложения. Эта замена приводит к сокращению на два прыжка.

Как работает инверсия цикла и как она оптимизирует наш кодовый путь?

NB: Было бы здорово, если бы кто-нибудь смог объяснить на примере кода Java, как JIT оптимизирует его для нативного кода и почему он оптимален для современных процессоров.

Пытаюсь
источник
2
Это не то, что вы сделали бы со своим исходным кодом. Это происходит на уровне нативного кода.
Марко Топольник
2
@MarkoTopolnik я знаю. Но я хочу знать, как JIT делает это на уровне собственного кода. Спасибо.
Попытка
1
о, круто, об этом есть страница в Википедии с множеством примеров en.wikipedia.org/wiki/Loop_inversion . Пример C так же действителен для Java.
Бенджамин Грюнбаум
Некоторое время назад, вдохновленный одним из вопросов по SO, я провел небольшое исследование в этом вопросе, возможно, результаты будут вам полезны: stackoverflow.com/questions/16205843/java-loop-efficiency/…
Adam Siemion
Это то же самое, что условие цикла обычно ставится в конце (независимо от того, будет ли выполнено меньше переходов), просто для того, чтобы было меньше инструкций перехода (1 против 2 на итерацию)?
extremeaxe5

Ответы:

108
while (condition) { 
  ... 
}

Рабочий процесс:

  1. проверить состояние;
  2. если false, перейти за пределы цикла;
  3. выполнить одну итерацию;
  4. перейти наверх.

if (condition) do {
  ...
} while (condition);

Рабочий процесс:

  1. проверить состояние;
  2. если false, перейти за пределы цикла;
  3. выполнить одну итерацию;
  4. проверить состояние;
  5. если это так, переходите к шагу 3.

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

Переходы в современных конвейерных архитектурах ЦП могут быть довольно дорогими: поскольку ЦП завершает выполнение проверок перед переходом, инструкции после этого перехода уже находятся в середине конвейера. Вся эта обработка должна быть отброшена, если предсказание ветвления не удается. Дальнейшее выполнение откладывается на время перезапуска конвейера.

Объяснение упомянутого предсказания перехода : для каждого вида условного перехода ЦП имеет две инструкции, каждая из которых включает ставку на результат. Например, вы можете поместить инструкцию « прыгать, если не ноль, ставка не ноль » в конце цикла, потому что прыжок должен быть выполнен на всех итерациях, кроме последней. Таким образом, ЦП начинает прокачивать свой конвейер инструкциями, следующими за целью перехода, а не инструкциями, следующими за самой инструкцией перехода.

Важная заметка

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

Марко Топольник
источник
51
Эта записка в конце действительно очень, очень важна.
TJ Crowder
2
@AdamSiemion: байт-код, созданный для данного do-whileисходного кода, не имеет значения, потому что мы фактически не пишем его. Мы пишем whileцикл и позволяем компилятору и JIT сговориться, чтобы улучшить его для нас (посредством инверсии цикла), если / по мере необходимости.
TJ Crowder
1
@TJCrowder +1 для вышеизложенного, плюс примечание Адаму: никогда не принимайте во внимание байт-код, думая об оптимизации JIT-компилятора. Байт-код намного ближе к исходному коду Java, чем к фактическому исполняемому JIT-скомпилированному коду. Фактически, в современных языках тенденция состоит в том, чтобы вообще не использовать байт-код как часть спецификации языка.
Марко Топольник
1
Было бы очень информативно. Важное примечание было объяснено немного подробнее. Почему это было бы полностью ошибочным?
arsaKasra
2
@arsaKasra Это заблуждение, потому что в целом удобочитаемость и стабильность важнее оптимизации исходного кода. Особенно с открытием того, что JIT делает это за вас, вы не должны пытаться (очень микро) оптимизировать самостоятельно.
Radiodef
24

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

В этом случае обычный whileцикл всегда будет возвращаться к началу хотя бы один раз и перескакивать до конца один раз в конце. Пример однократного выполнения простого цикла:

int i = 0;
while (i++ < 1) {
    //do something
}  

С do-whileдругой стороны, цикл пропустит первый и последний прыжок. Вот цикл, эквивалентный приведенному выше, который будет работать без скачков:

int i = 0;
if (i++ < 1) {
    do {
        //do something
    } while (i++ < 1); 
}
Кеппил
источник
+1 за правильность и во-первых, рассмотрите возможность добавления примера кода. Нечто подобное , boolean b = true; while(b){ b = maybeTrue();}чтобы boolean b;do{ b = maybeTrue();}while(b);должно хватить.
Бенджамин Грюнбаум
Без проблем. Это как бы аннулирует начальную строку ответа, fwiw. :-)
TJ Crowder
@TJ Ну, он все равно не оптимизирует цикл, в который не вошел, в обоих случаях будет один прыжок.
Keppil
О да. Извините, я читал это, чтобы означать, что вы не можете применить его к циклам, которые не повторялись хотя бы один раз (а не то, что это им не помогает). С тобой сейчас. :-)
TJ Crowder
@Keppil Вы, вероятно, должны явно указать, что в случае, когда у нас есть большое количество итераций X, мы сохраним только один прыжок среди X.
Мануэль Сельва
3

Пройдемся по ним:

whileВерсия:

void foo(int n) {
    while (n < 10) {
       use(n);
       ++n;
    }
    done();
}
  1. Сначала мы проверяем nи переходим к тому, done();не выполняется ли условие.
  2. Затем мы используем и увеличиваем n.
  3. Теперь вернемся к условию.
  4. Промыть, повторить.
  5. Когда условие больше не выполняется, мы переходим к done().

do-whileВерсия:

(Помните, что на самом деле мы не делаем этого в исходном коде [который может вызвать проблемы с обслуживанием], компилятор / JIT делает это за нас.)

void foo(int n) {
    if (n < 10) {
        do {
            use(n);
            ++n;
        }
        while (n < 10);
    }
    done();
}
  1. Сначала мы проверяем nи переходим к тому, done();не выполняется ли условие.
  2. Затем мы используем и увеличиваем n.
  3. Теперь мы проверяем условие и возвращаемся назад, если оно истинно.
  4. Промыть, повторить.
  5. Когда условие больше не выполняется, мы перетекаем (не перескакиваем) на done().

Так, например, если nначинает быть 9, мы никогда не перескакиваем в do-whileверсии, тогда как в whileверсии мы должны вернуться к началу, провести тест, а затем вернуться к концу, когда мы увидим, что это не так. .

TJ Crowder
источник
3

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

Эта ссылка представляет собой еще один пример инверсии цикла. В некоторых архитектурах, где декремент и сравнение реализованы как единый набор инструкций, имеет смысл преобразовать цикл for в цикл while с операциями декремента и сравнения.

В Википедии есть очень хороший пример, и я снова объясняю его здесь.

 int i, a[100];
  i = 0;
  while (i < 100) {
    a[i] = 0;
    i++;
  }

будет преобразован компилятором в

  int i, a[100];
  i = 0;
  if (i < 100) {
    do {
      a[i] = 0;
      i++;
    } while (i < 100);
  }

Как это влияет на производительность? Когда значение i равно 99, процессору не нужно выполнять GOTO (что требуется в первом случае). Это улучшает производительность.

Анирудхан Дж.
источник