Раскрутка петли

редактировать
Не путать с раскручиванием стека.

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

Целью раскручивания цикла является увеличение скорости программы за счет сокращения или исключения инструкций, управляющих циклом, таких как арифметические операции с указателями и тесты «конца цикла» на каждой итерации; снижение отраслевых штрафов; а также сокрытие задержек, включая задержку чтения данных из памяти. Чтобы устранить эти вычислительные издержки, циклы можно переписать как повторяющуюся последовательность подобных независимых операторов.

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

Содержание

  • 1 Преимущества
  • 2 Недостатки
  • 3 Статическая / ручная раскрутка петли
    • 3.1 Простой ручной пример на C
    • 3.2 Ранняя сложность
    • 3.3 Разворачивание циклов WHILE
  • 4 Динамическое разворачивание
    • 4.1 Пример ассемблера (IBM / 360 или Z / Architecture)
    • 4.2 Пример C
    • 4.3 Пример развертывания цикла с языка ассемблера C на MIPS [9]
      • 4.3.1 Преобразование на язык ассемблера MIPS
      • 4.3.2 Развертывание цикла в MIPS
  • 5 См. Также
  • 6 Ссылки
  • 7 Дальнейшее чтение
  • 8 Внешние ссылки

Преимущества

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

  • Значительный выигрыш может быть получен, если сокращение числа выполняемых инструкций компенсирует любое снижение производительности, вызванное увеличением размера программы.
  • Штраф за переход минимален.
  • Если операторы в цикле независимы друг от друга (т. Е. Если операторы, которые появляются раньше в цикле, не влияют на операторы, следующие за ними), эти операторы потенциально могут выполняться параллельно.
  • Может быть реализован динамически, если количество элементов массива неизвестно во время компиляции (как в устройстве Даффа ).

Оптимизирующие компиляторы иногда выполняют развертывание автоматически или по запросу.

Недостатки

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

Статическая / ручная раскрутка петли

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

Простой ручной пример на C

Процедура в компьютерной программе заключается в удалении 100 элементов из коллекции. Обычно это выполняется с помощью for цикла, который вызывает функцию delete (item_number). Если эта часть программы должна быть оптимизирована, а накладные расходы цикла требуют значительных ресурсов по сравнению с таковыми для функции delete (x), для его ускорения можно использовать раскрутку.

Нормальный цикл После разворачивания цикла
 int x; for (x = 0; x lt; 100; x++) { delete(x); }
 int x; for (x = 0; x lt; 100; x += 5) { delete(x); delete(x + 1); delete(x + 2); delete(x + 3); delete(x + 4); }

В результате этой модификации новая программа должна сделать только 20 итераций вместо 100. После этого необходимо выполнить только 20% переходов и условных переходов, что представляет собой, на протяжении многих итераций, потенциально значительное уменьшение накладные расходы на администрирование цикла. Для получения оптимального результата в развернутом коде не следует указывать переменные, требующие арифметики указателей. Обычно для этого требуется адресация « основание плюс смещение», а не индексированная ссылка.

С другой стороны, это ручное развертывание цикла увеличивает размер исходного кода с 3 строк до 7, которые должны быть созданы, проверены и отлажены, и компилятору, возможно, придется выделить больше регистров для хранения переменных в расширенной итерации цикла. Кроме того, переменные управления циклом и количество операций внутри развернутой структуры цикла должны быть тщательно выбраны, чтобы результат действительно был таким же, как и в исходном коде (при условии, что это более поздняя оптимизация уже работающего кода). Например, рассмотрите последствия, если количество итераций не делится на 5. Требуемые ручные поправки также становятся несколько более сложными, если условия теста являются переменными. См. Также устройство Даффа.

Ранняя сложность

В простом случае управление циклом - это просто административные накладные расходы, которые организуют продуктивные операторы. Сам цикл не вносит никакого вклада в желаемые результаты, просто избавляя программиста от утомительного повторения кода сотни раз, что можно было бы сделать с помощью препроцессора, генерирующего репликации, или текстового редактора. Точно так же if операторы -statements и другие операторы управления потоком могут быть заменены репликацией кода, за исключением того, что результатом может быть раздувание кода. Компьютерные программы легко отслеживают комбинации, но программисты считают это повторение скучным и допускают ошибки. Рассматривать:

Нормальный цикл После разворачивания цикла
for i := 1:8 do if i mod 2 = 0 then do_even_stuff(i) else do_odd_stuff(i); next i;
do_odd_stuff(1); do_even_stuff(2); do_odd_stuff(3); do_even_stuff(4); do_odd_stuff(5); do_even_stuff(6); do_odd_stuff(7); do_even_stuff(8);

Но, конечно, выполняемый код не обязательно должен быть вызовом процедуры, и в следующем примере в вычислении используется индексная переменная:

Нормальный цикл После разворачивания цикла
x(1) := 1; For i := 2:9 do x(i) := x(i - 1) * i; print i, x(i); next i;
x(1) := 1; x(2) := x(1) * 2; print 2, x(2); x(3) := x(2) * 3; print 3, x(3); x(4) := x(3) * 4; print 4, x(4);... etc.

который в случае компиляции может привести к появлению большого количества кода ( печально известны операторы печати ), но возможна дальнейшая оптимизация. В этом примере делается ссылка только на x (i) и x (i - 1) в цикле (последний только для разработки нового значения x (i) ), поэтому, учитывая, что нет более поздней ссылки на массив x, разработанный здесь, его использование может быть заменено простой переменной. Однако такое изменение будет означать простую переменную, значение которой изменяется, тогда как, оставаясь с массивом, анализ компилятора может отметить, что значения массива являются постоянными, каждое из которых получено из предыдущей константы, и поэтому переносит постоянные значения так, чтобы код становится

print 2, 2; print 3, 6; print 4, 24;...etc.

В общем, содержимое цикла может быть большим, включая сложную индексацию массивов. Эти случаи, вероятно, лучше всего оставить для развертывания оптимизирующим компиляторам. Репликация самых внутренних циклов может позволить множество возможных оптимизаций, но даст лишь небольшой выигрыш, если n не велико.

Развертывание циклов WHILE

Рассмотрим цикл псевдокода WHILE, подобный следующему:

Нормальный цикл После разворачивания цикла Развернутая и "доработанная" петля
WHILE (condition) DO action ENDWHILE......
WHILE (condition) DO action IF NOT(condition) THEN GOTO break action IF NOT(condition) THEN GOTO break action ENDWHILE LABEL break:.
IF (condition) THEN REPEAT action IF NOT(condition) THEN GOTO break action IF NOT(condition) THEN GOTO break action WHILE (condition) LABEL break:

В этом случае разворачивание происходит быстрее, поскольку ENDWHILE (переход к началу цикла) будет выполняться на 66% реже.

Еще лучше, пример «настроенного» псевдокода, который может выполняться автоматически некоторыми оптимизирующими компиляторами, полностью исключая безусловные переходы.

Динамическое разворачивание

Поскольку преимущества развертывания цикла часто зависят от размера массива, который часто может быть неизвестен до времени выполнения, JIT- компиляторы (например) могут определить, вызывать ли «стандартную» последовательность цикла или вместо этого генерировать (относительно короткий) последовательность индивидуальных инструкций для каждого элемента. Такая гибкость является одним из преимуществ своевременных методов по сравнению со статической или ручной оптимизацией в контексте развертывания цикла. В этой ситуации часто бывает с относительно небольшими значениями n, когда экономия по-прежнему полезна - требуется довольно небольшое (если вообще есть) общее увеличение размера программы (которое может быть включено только один раз как часть стандартной библиотеки).

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

Пример ассемблера (IBM / 360 или Z / Architecture)

Пример x86 см. В разделе « Внешние ссылки ».

Этот пример предназначен для ассемблеров IBM / 360 или Z / Architecture и предполагает, что поле размером 100 байтов (при нулевом смещении) должно быть скопировано из массива FROM в массив TO - оба имеют 50 записей с длиной элемента 256 байтов каждая.

* The return address is in R14. * Initialize registers R15, R0, R1, and R2 from data defined at the end of * the program starting with label INIT/MAXM1.   LM R15,R2,INIT     Set R15 = maximum number of MVC *           instructions (MAXM1 = 16), *           R0 = number of entries of array, *           R1 = address of 'FROM' array, and *           R2 = address of 'TO' array. * * The loop starts here. LOOP  EQU *       Define LOOP label. * At this point, R15 will always contain the number 16 (MAXM1).   SR R15,R0      Subtract the remaining number of *           entries in the array (R0) from R15.   BNP ALL       If R15 is not positive, meaning we *           have more than 16 remaining entries *           in the array, jump to do the entire *           MVC sequence and then repeat. * * Calculate an offset (from start of MVC sequence) for unconditional branch to * the 'unwound' MVC loop below. * If the number of remaining entries in the arrays is zero, R15 will be 16, so * all the MVC instructions will be bypassed.   MH R15,=AL2(ILEN)    Multiply R15 by the length of one *           MVC instruction.   B  ALL(R15)      Jump to ALL+R15, the address of the *           calculated specific MVC instruction *           with drop through to the rest of them. * * MVC instruction 'table'. * First entry has maximum allowable offset with single register = hexadecimal F00 * (15*256) in this example. * All 16 of the following MVC ('move character') instructions use base-plus-offset * addressing and each to/from offset decreases by the length of one array element * (256). This avoids pointer arithmetic being required for each element up to a * maximum permissible offset within the instruction of hexadecimal FFF * (15*256+255). The instructions are in order of decreasing offset, so the last * element in the set is moved first. ALL  MVC 15*256(100,R2),15*256(R1) Move 100 bytes of 16th entry from *           array 1 to array 2 (with *           drop-through). ILEN  EQU *-ALL      Set ILEN to the length of the previous *           MVC instruction.   MVC 14*256(100,R2),14*256(R1) Move 100 bytes of 15th entry.   MVC 13*256(100,R2),13*256(R1) Move 100 bytes of 14th entry.   MVC 12*256(100,R2),12*256(R1) Move 100 bytes of 13th entry.   MVC 11*256(100,R2),11*256(R1) Move 100 bytes of 12th entry.   MVC 10*256(100,R2),10*256(R1) Move 100 bytes of 11th entry.   MVC 09*256(100,R2),09*256(R1) Move 100 bytes of 10th entry.   MVC 08*256(100,R2),08*256(R1) Move 100 bytes of 9th entry.   MVC 07*256(100,R2),07*256(R1) Move 100 bytes of 8th entry.   MVC 06*256(100,R2),06*256(R1) Move 100 bytes of 7th entry.   MVC 05*256(100,R2),05*256(R1) Move 100 bytes of 6th entry.   MVC 04*256(100,R2),04*256(R1) Move 100 bytes of 5th entry.   MVC 03*256(100,R2),03*256(R1) Move 100 bytes of 4th entry.   MVC 02*256(100,R2),02*256(R1) Move 100 bytes of 3rd entry.   MVC 01*256(100,R2),01*256(R1) Move 100 bytes of 2nd entry.   MVC 00*256(100,R2),00*256(R1) Move 100 bytes of 1st entry. *   S  R0,MAXM1      Reduce the number of remaining entries *           to process.   BNPR R14       If no more entries to process, return *           to address in R14.   AH R1,=AL2(16*256)    Increment 'FROM' array pointer beyond *           first set.   AH R2,=AL2(16*256)    Increment 'TO' array pointer beyond *           first set.   L  R15,MAXM1     Reload the maximum number of MVC *           instructions per batch into R15 *           (destroyed by the calculation in the *           first instruction of the loop).   B  LOOP       Execute loop again. * * Static constants and variables (these could be passed as parameters, except * MAXM1). INIT  DS 0A       4 addresses (pointers) to be *           pre-loaded with the 'LM' instruction *           in the beginning of the program. MAXM1 DC A(16)      Maximum number of MVC instructions *           executed per batch. N  DC A(50)      Number of actual entries in array (a *           variable, set elsewhere).   DC A(FROM)      Address of start of array 1 *           ("pointer").   DC A(TO)      Address of start of array 2 *           ("pointer"). * * Static arrays (these could be dynamically acquired). FROM  DS 50CL256      Array of 50 entries of 256 bytes each. TO  DS 50CL256      Array of 50 entries of 256 bytes each.

В этом примере для «обычного» цикла (50 итераций) потребуется приблизительно 202 инструкции, тогда как для приведенного выше динамического кода потребуется всего около 89 инструкций (или примерно 56% экономии). Если бы массив состоял только из двух записей, он все равно выполнялся бы примерно за то же время, что и исходный развернутый цикл. Увеличение размера кода составляет всего около 108 байт, даже если в массиве тысячи записей.

Конечно, аналогичные методы могут использоваться, когда задействовано несколько инструкций, при условии, что длина объединенных инструкций регулируется соответствующим образом. Например, в этом же примере, если требуется очистить оставшуюся часть каждой записи массива до нулей сразу после копирования 100-байтового поля XC xx*256+100(156,R1),xx*256+100(R2), можно добавить дополнительную инструкцию очистки сразу после каждого MVC в последовательности (где xx соответствует значение в MVC над ним).

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

Пример C

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

#include lt;stdio.hgt; /* The number of entries processed per loop iteration.      */ /* Note that this number is a 'constant constant' reflecting the code below. */ #define BUNCHSIZE (8) int main(void) { int i = 0;         /* counter */ int entries = 50;        /* total number to process */ int repeat;         /* number of while repetitions*/ int left = 0;         /* remainder (process later) */ /* If the number of elements is not be divisible by BUNCHSIZE,    */ /* get repeat times required to do most processing in the while loop  */ repeat = (entries / BUNCHSIZE);    /* number of times to repeat */ left = (entries % BUNCHSIZE);    /* calculate remainder  */ /* Unroll the loop in 'bunches' of 8          */ while (repeat--) { printf("process(%d)\n", i ); printf("process(%d)\n", i + 1); printf("process(%d)\n", i + 2); printf("process(%d)\n", i + 3); printf("process(%d)\n", i + 4); printf("process(%d)\n", i + 5); printf("process(%d)\n", i + 6); printf("process(%d)\n", i + 7); /* update the index by amount processed in one go       */ i += BUNCHSIZE; } /* Use a switch statement to process remaining by jumping to the case label */ /* at the label that will then drop through to complete the set    */ switch (left) { case 7: printf("process(%d)\n", i + 6); /* process and rely on drop              through     */ case 6: printf("process(%d)\n", i + 5); case 5: printf("process(%d)\n", i + 4); case 4: printf("process(%d)\n", i + 3); case 3: printf("process(%d)\n", i + 2); case 2: printf("process(%d)\n", i + 1); /* two left     */ case 1: printf("process(%d)\n", i);  /* just one left to process */ case 0: ;         /* none left     */ } }

Дублирования кода можно было избежать, записав две части вместе, как в устройстве Даффа.

Пример развертывания цикла с языка ассемблера C на MIPS

В следующем примере будет вычислено скалярное произведение двух векторов типа A и B со 100 элементами double. Вот код на C:

double dotProduct = 0; for (int i = 0; i lt; 100; i++) { dotProduct += A[i]*B[i]; }

Преобразование на язык ассемблера MIPS

Ниже приведен ассемблерный код MIPS, который вычисляет скалярное произведение двух векторов из 100 входов, A и B, перед реализацией развертывания цикла. В приведенном ниже коде отсутствуют инициализации цикла:

  • Инициализируйте количество циклов (7 долларов США) до 100.
  • Инициализировать скалярное произведение ($ f10) равным 0.
  • Инициализируйте A[i] указатель ($ 5) на базовый адрес A.
  • Инициализируйте B[i] указатель ($ 6) на базовый адрес B.

Обратите внимание, что размер одного элемента массивов (а double) составляет 8 байтов.

 loop3:    l.d  $f10, 0($5)  ; $f10 ← A[i]    l.d  $f12, 0($6)  ; $f12 ← B[i]    mul.d $f10, $f10, $f12 ; $f10 ← A[i]*B[i]    add.d $f8, $f8, $f10 ; $f8 ← $f8 + A[i]*B[i]    addi $5, $5, 8   ; increment pointer for A[i] by the size          ; of a double.    addi $6, $6, 8   ; increment pointer for B[i] by the size          ; of a double.    addi $7, $7, -1  ; decrement loop count  test:    bgtz $7, loop3   ; Continue if loop count gt; 0

Развертывание цикла в MIPS

Далее следует то же самое, что и выше, но с реализацией развертывания цикла с коэффициентом 4. Еще раз обратите внимание, что размер одного элемента массивов (a double) составляет 8 байтов; таким образом, 0, 8, 16, 24 смещения и 32 смещения на каждой петле.

 loop3:    l.d  $f10, 0($5)   ; iteration with displacement 0    l.d  $f12, 0($6)    mul.d $f10, $f10, $f12    add.d $f8, $f8, $f10    l.d  $f10, 8($5)   ; iteration with displacement 8    l.d  $f12, 8($6)    mul.d $f10, $f10, $f12    add.d $f8, $f8, $f10    l.d  $f10, 16($5)  ; iteration with displacement 16    l.d  $f12, 16($6)    mul.d $f10, $f10, $f12    add.d $f8, $f8, $f10    l.d  $f10, 24($5)  ; iteration with displacement 24    l.d  $f12, 24($6)    mul.d $f10, $f10, $f12    add.d $f8, $f8, $f10    addi $5, $5, 32    addi $6, $6, 32    addi $7, $7, -4  test:    bgtz $7, loop3   ; Continue loop if $7 gt; 0

Смотрите также

Рекомендации

дальнейшее чтение

внешние ссылки

Последняя правка сделана 2023-04-17 01:56:34
Содержание доступно по лицензии CC BY-SA 3.0 (если не указано иное).
Обратная связь: support@alphapedia.ru
Соглашение
О проекте