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

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

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

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

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


person Trying    schedule 29.12.2013    source источник
comment
Это не то, что вы бы сделали со своим исходным кодом. Это происходит на уровне нативного кода.   -  person Marko Topolnik    schedule 29.12.2013
comment
@MarkoTopolnik я знаю. Но я хочу знать, как JIT делает это на уровне собственного кода. Спасибо.   -  person Trying    schedule 29.12.2013
comment
о, круто, об этом есть страница в Википедии с множеством примеров en.wikipedia.org/wiki/Loop_inversion < / а>. Пример C так же действителен для Java.   -  person Benjamin Gruenbaum    schedule 29.12.2013
comment
Это то же самое, что условие цикла обычно ставится в конце (независимо от того, будет ли выполнено меньше переходов), просто для того, чтобы было меньше инструкций перехода (1 против 2 на итерацию)?   -  person extremeaxe5    schedule 13.04.2019


Ответы (4)


while (condition) { 
  ... 
}

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

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

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

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

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

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

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

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

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

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

person Marko Topolnik    schedule 29.12.2013
comment
Эта записка в конце действительно очень, очень важна. - person T.J. Crowder; 29.12.2013
comment
@MarkoTopolnik Вы уверены, что сгенерированный байт-код для цикла do while будет содержать именно те переходы, которые вы описали? - person Adam Siemion; 29.12.2013
comment
@AdamSiemion: байт-код, сгенерированный для данного do-while исходного кода, не имеет значения, потому что мы на самом деле его не пишем. Мы пишем цикл while и позволяем компилятору и JIT сговориться, чтобы улучшить его для нас (через инверсию цикла), если / по мере необходимости. - person T.J. Crowder; 29.12.2013
comment
@ T.J.Crowder +1 за вышеизложенное, плюс примечание Адаму: никогда не учитывать байт-код, думая об оптимизации JIT-компилятора. Байт-код намного ближе к исходному коду Java, чем к фактическому исполняемому JIT-скомпилированному коду. Фактически, в современных языках тенденция состоит в том, чтобы вообще не использовать байт-код как часть спецификации языка. - person Marko Topolnik; 29.12.2013
comment
Было бы более информативным, если бы Важное примечание было объяснено немного подробнее. Почему это должно быть полностью ошибочным? - person arsaKasra; 29.12.2013
comment
@arsaKasra Это подразумевает сам вопрос: то, что мы здесь описываем, является преобразованием, выполняемым JIT-компилятором, и я думаю, что понятно, почему кто-то предпочел бы не видеть вторая форма в коде. - person Marko Topolnik; 29.12.2013
comment
@MarkoTopolnik Просто так, как вы это выразились, и так, как T.J. подчеркнули, что это сделало это потенциально очень плохим. Я не мог понять, что в этом было бы технически не так, хотя я не выгляжу достаточно проницательным, чтобы не предпочесть это первой форме. - person arsaKasra; 29.12.2013
comment
@arsaKasra Это заблуждение, потому что в целом удобочитаемость и стабильность важнее оптимизации исходного кода. Особенно с открытием того, что JIT делает это за вас, вы не должны пытаться (очень микро) оптимизировать самостоятельно. - person Radiodef; 30.12.2013
comment
Я не вижу разницы между двумя версиями. В обоих случаях выполняется ровно N + 1 условных переходов, когда цикл повторяется N раз (N ›= 0). Безусловные переходы не следует учитывать, так как они не могут быть предсказаны неверно. Что мне не хватает? - person Yves Daoust; 02.01.2014
comment
Вам не хватает количества фактических переходов, сделанных на уровне машинного кода. По общему признанию, исходный код ничего не определяет на этом уровне, но вы можете представить, как могла бы выглядеть наивная версия. - person Marko Topolnik; 02.01.2014

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

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

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

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

int i = 0;
if (i++ < 1) {
    do {
        //do something
    } while (i++ < 1); 
}
person Keppil    schedule 29.12.2013
comment
+1 за правильность и во-первых, рассмотрите возможность добавления примера кода. Что-то вроде от boolean b = true; while(b){ b = maybeTrue();} до boolean b;do{ b = maybeTrue();}while(b); должно хватить. - person Benjamin Gruenbaum; 29.12.2013
comment
Не стоит беспокоиться. Это как бы аннулирует начальную строку ответа, fwiw. :-) - person T.J. Crowder; 29.12.2013
comment
@ T.J. Ну, он все равно не оптимизирует цикл, в который не вошел, в обоих случаях будет один прыжок. - person Keppil; 29.12.2013
comment
О да. Извините, я читал это, чтобы означать, что вы не могли применить его к циклам, которые не повторялись хотя бы один раз (а не то, что это им не помогает). С тобой сейчас. :-) - person T.J. Crowder; 29.12.2013
comment
@Keppil Вам, вероятно, следует явно указать, что в случае, когда у нас есть большое количество итераций X, мы сохраним только один прыжок среди X. - person Manuel Selva; 26.10.2016

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

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

person T.J. Crowder    schedule 29.12.2013

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

Эта ссылка представляет собой еще один пример инверсии цикла. В некоторых архитектурах, где декремент и сравнение реализованы как единый набор инструкций, имеет смысл преобразовать цикл 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 (что требуется в первом случае). Это улучшает производительность.

person Anirudhan J    schedule 29.12.2013