Що таке техніка інверсії петлі?


89

Я переглядав документ, який розповідає про методи оптимізації компілятора (JIT) для Java. Одним з них була "інверсія петлі". І в документі сказано:

Ви замінюєте звичайний whileцикл на do-whileцикл. І do-whileцикл встановлюється в ifреченні. Ця заміна призводить до двох менших стрибків.

Як працює інверсія циклу і як вона оптимізує наш шлях до коду?

Примітка: Було б чудово, якщо хтось може пояснити на прикладі коду Java і як JIT оптимізує його до власного коду і чому це оптимально в сучасних процесорах.


2
Це не те, що ви зробили б із вихідним кодом. Це відбувається на рівні власного коду.
Марко Топольник

2
@MarkoTopolnik я знаю. Але я хочу знати, як JIT робить це на рівні рідного коду. Дякую.
Спроба

1
о круто, про це є сторінка wikipedia з безліччю прикладів en.wikipedia.org/wiki/Loop_inversion . Приклад C так само справедливий у Java.
Бенджамін Груенбаум,

Деякий час назад натхненний одним з питань на SO I провели коротке дослідження з цього питання, можливо , результати будуть корисні для вас: stackoverflow.com/questions/16205843/java-loop-efficiency / ...
Адам Siemion

Це те саме, що і там, де умова циклу зазвичай ставиться в кінці (незалежно від того, чи буде виконано менше стрибків), просто щоб було менше інструкцій щодо стрибка (1 проти 2 на ітерацію)?
extremeaxe5

Відповіді:


108
while (condition) { 
  ... 
}

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

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

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

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

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

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

Стрибки на сучасних конвеєрних архітектурах процесорів можуть бути досить дорогими: оскільки центральний процесор закінчує виконання перевірок перед стрибком, інструкції після цього стрибка вже знаходяться в середині конвеєра. Вся ця обробка повинна бути відкинута, якщо передбачення гілки не вдається. Подальше виконання затримується, поки трубопровід реформується.

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

Важлива примітка

Будь ласка , НЕ приймайте це як приклад того , як оптимізувати на рівні вихідного коду. Це було б цілком помилковим, оскільки, як вже зрозуміло з Вашого запитання, перетворення з першої форми у другу - це те, що компілятор JIT робить, як правило, повністю самостійно.


51
Ця примітка в кінці - це дуже, дуже важлива річ.
TJ Crowder

2
@AdamSiemion: Байт-код, згенерований для даного do-whileвихідного коду, не має значення, оскільки ми насправді цього не пишемо. Ми пишемо whileцикл і дозволяємо компілятору та JIT змовлятись, щоб покращити його для нас (за допомогою інверсії циклу), якщо / за необхідності.
TJ Crowder

1
@TJCrowder +1 для вищесказаного, плюс примітка до Адама: ніколи не розглядайте байт-код, думаючи про оптимізацію компілятора JIT. Байт-код набагато ближчий до вихідного коду Java, ніж до фактично виконуваного JIT-коду. Насправді тенденція в сучасних мовах полягає в тому, щоб байт-код взагалі не був частиною мовної модифікації.
Marko Topolnik

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 Ну, це все одно не буде оптимізувати цикл, який не введено, в обох випадках буде один стрибок.
Кеппіл,

Так, так. На жаль, я читав це, щоб означати, що ви не можете застосувати його до циклів, які не циклювались хоча б один раз (а не що це їм не допомагає). З тобою зараз. :-)
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версії ми маємо перейти назад на початок, зробити тест, а потім перейти назад до кінця, коли побачимо, що це неправда .


3

Інверсія циклу - це техніка оптимізації продуктивності, яка покращує продуктивність, оскільки процесор може досягти того самого результату за меншої кількості інструкцій. Це головним чином повинно покращити ефективність роботи в граничних умовах.

Це посилання пропонує ще один приклад для інверсії циклу. У деяких архітектурах, де декрементування та порівняння реалізовано як єдиний набір інструкцій, має сенс перетворити цикл for на деякий час за допомогою операції декрементування та порівняння.

У Вікіпедії є дуже хороший приклад, і я ще раз пояснюю це тут.

 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 (що потрібно в першому випадку). Це покращує продуктивність.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.