Развертывание петли - Loop unrolling

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

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

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

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

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

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

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

Недостатки

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

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

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

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

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

Нормальный циклПосле разворачивания цикла
 int Икс; за (Икс = 0; Икс < 100; Икс++) {     Удалить(Икс); }
 int Икс;  за (Икс = 0; Икс < 100; Икс += 5 ) {     Удалить(Икс);     Удалить(Икс + 1);     Удалить(Икс + 2);     Удалить(Икс + 3);     Удалить(Икс + 4); }

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

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

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

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

Нормальный циклПосле разворачивания цикла
for i: = 1: 8 do if i mod 2 = 0 then do_even_stuff (i) else do_odd_stuff (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; Для i: = 2: 9 выполните x (i): = x (i - 1) * i; напечатайте i, x (i); следующий я;
х (1): = 1; х (2): = х (1) * 2; напечатайте 2, x (2); x (3): = x (2) * 3; напечатайте 3, x (3); x (4): = x (3) * 4; print 4, x (4); ... и т. д.

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

печать 2, 2; печать 3, 6; печать 4, 24; ... и т. д.

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

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

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

Нормальный циклПосле разворачивания циклаРазвернутая и "доработанная" петля
WHILE (условие) DO actionENDWHILE ......
WHILE (условие) DO action IF NOT (условие) THEN GOTO break action IF NOT (условие) THEN GOTO break actionENDWHILELABEL break :.
ЕСЛИ (условие) ТО ПОВТОРИТЬ действие ЕСЛИ НЕ (условие) ТО НАЙТИ прервать действие ЕСЛИ НЕ (условие) ТО НАЙТИ прервать действие ПОКА (условие) LABEL break:

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

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

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

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

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

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

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

 1 * Обратный адрес указан в R14. 2 * Инициализировать регистры R15, R0, R1 и R2 из данных, определенных в конце  3 * программа начинается с метки INIT / MAXM1. 4          LM R15, R2, INIT Установить R15 = максимальное количество MVC 5 * инструкции (MAXM1 = 16),  6 * R0 = количество записей массива, 7 * R1 = адрес массива FROM, и 8 * R2 = адрес массива TO. 9 *10 * Цикл начинается здесь.11 LOOP EQU * Определите метку LOOP.12 * На этом этапе R15 всегда будет содержать число 16 (MAXM1).13          SR R15, R0 Вычтите оставшееся количество 14 * записи в массиве (R0) от R15.15          BNP ALL Если R15 не положительный, это означает, что мы16 * осталось более 16 записей17 * в массиве перейдите, чтобы выполнить все18 * Последовательность MVC, а затем повторить.19 *20 * Вычислить смещение (от начала последовательности MVC) для безусловного перехода к 21 * «размотанный» цикл MVC ниже.22 * Если количество оставшихся записей в массивах равно нулю, R15 будет 16, поэтому 23 * все инструкции MVC будут пропущены.24          MH R15, = AL2 (ILEN) Умножить R15 на длину единицы.25 * Инструкция MVC.26          B ALL (R15) Перейти к ALL + R15, адресу27 * рассчитанная конкретная инструкция MVC 28 * с переходом к остальным.29 *30 * Таблица инструкций MVC. 31 * Первая запись имеет максимально допустимое смещение с одним регистром = шестнадцатеричное F0032 * (15 * 256) в этом примере.33 * Все 16 из следующих инструкций MVC ('перемещение символа') используют основание плюс смещение 34 * адресация и каждое смещение до / от уменьшается на длину одного элемента массива35 * (256). Это позволяет избежать арифметики указателей для каждого элемента до 36 * максимально допустимое смещение в пределах инструкции шестнадцатеричного FFF 37 * (15 * 256 + 255). Инструкции приведены в порядке убывания смещения, поэтому последний 38 * элемент в наборе перемещается первым.39 ALL MVC 15 * 256 (100, R2), 15 * 256 (R1) Переместить 100 байт 16-й записи из 40 * от массива 1 до массива 2 (с 41 * сквозное).42 ILEN EQU * -ALL Установить ILEN равным длине предыдущего43 * Инструкция MVC.44          MVC 14 * 256 (100, R2), 14 * 256 (R1) Перемещение 100 байтов 15-й записи.45          MVC 13 * 256 (100, R2), 13 * 256 (R1) Переместить 100 байтов 14-й записи.46          MVC 12 * 256 (100, R2), 12 * 256 (R1) Перемещение 100 байт 13-й записи.47          MVC 11 * 256 (100, R2), 11 * 256 (R1) Перемещение 100 байтов 12-й записи.48          MVC 10 * 256 (100, R2), 10 * 256 (R1) Перемещение 100 байт 11-й записи.49          MVC 09 * 256 (100, R2), 09 * 256 (R1) Переместить 100 байтов 10-й записи.50          MVC 08 * 256 (100, R2), 08 * 256 (R1) Переместить 100 байтов 9-й записи.51          MVC 07 * 256 (100, R2), 07 * 256 (R1) Переместить 100 байтов 8-й записи.52          MVC 06 * 256 (100, R2), 06 * 256 (R1) Переместить 100 байт 7-й записи.53          MVC 05 * 256 (100, R2), 05 * 256 (R1) Переместить 100 байтов 6-й записи.54          MVC 04 * 256 (100, R2), 04 * 256 (R1) Перемещение 100 байтов 5-й записи.55          MVC 03 * 256 (100, R2), 03 * 256 (R1) Переместить 100 байт 4-й записи.56          MVC 02 * 256 (100, R2), 02 * 256 (R1) Перемещение 100 байтов 3-й записи.57          MVC 01 * 256 (100, R2), 01 * 256 (R1) Переместить 100 байт 2-й записи.58          MVC 00 * 256 (100, R2), 00 * 256 (R1) Перемещение 100 байтов 1-й записи.59 *60          S R0, MAXM1 Уменьшить количество оставшихся записей61 *                                           обрабатывать.62          BNPR R14 Если записей для обработки больше нет, вернуть63 * обратиться в R14.64          AH R1, = AL2 (16 * 256) Увеличить указатель массива 'FROM' за пределы65 * первый комплект.66          AH R2, = AL2 (16 * 256) Увеличить указатель массива TO за пределами67 * первый комплект.68          L R15, MAXM1 Перезагрузить максимальное количество MVC 69 * инструкции на партию в R1570 * (уничтожено расчетом в 71 * первая инструкция цикла).72          B LOOP Повторное выполнение цикла.73 *74 * Статические константы и переменные (их можно передавать как параметры, кроме 75 * MAXM1).76 INIT DS 0A 4 адреса (указатели) должны быть 77 * предварительно загружен с инструкцией 'LM'78 * в начале программы.79 MAXM1 DC A (16) Максимальное количество инструкций MVC80 * выполняется за партию.81 N DC A (50) Количество фактических записей в массиве (a 82 * переменная, установленная в другом месте).83          DC A (FROM) Адрес начала массива 1 84 * («указатель»).85          DC A (TO) Адрес начала массива 2 86 * («указатель»).87 *88 * Статические массивы (могут быть получены динамически).89 FROM DS 50CL256 Массив из 50 записей по 256 байт каждая.90 TO DS 50CL256 Массив из 50 записей по 256 байт каждая.

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

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

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

Пример C

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

#включают <stdio.h>/ * Количество записей, обработанных за итерацию цикла. * // * Обратите внимание, что это число является «постоянной константой», отражающей приведенный ниже код. * /#define BUNCHSIZE (8)int основной(пустота){   int я = 0;                                    /* прилавок */  int записи = 50;                             / * общее количество для обработки * /  int повторение;                                   / * количество повторов while * /  int оставили = 0;                                 / * остаток (обработать позже) * /    / * Если количество элементов не делится на BUNCHSIZE, * /   / * получаем время повтора, необходимое для выполнения большей части обработки в цикле while * /  повторение = (записи / РАЗМЕР);                / * количество повторений * /  оставили   = (записи % РАЗМЕР);                / * вычисляем остаток * /  / * Разворачиваем цикл «пучками» по 8 * /   пока (повторение--)   {     printf("процесс (% d) п", я    );    printf("процесс (% d) п", я + 1);     printf("процесс (% d) п", я + 2);     printf("процесс (% d) п", я + 3);     printf("процесс (% d) п", я + 4);     printf("процесс (% d) п", я + 5);     printf("процесс (% d) п", я + 6);     printf("процесс (% d) п", я + 7);    / * обновляем индекс по количеству обработанных за раз * /     я += РАЗМЕР;  }  / * Используйте оператор switch для обработки оставшегося, перейдя к метке case * /   / * на метке, которая затем пропадет, чтобы завершить набор * /   выключатель (оставили)   {     кейс 7 : printf("процесс (% d) п", я + 6);   / * обрабатываем и полагаемся на drop                                                    через                    */     кейс 6 : printf("процесс (% d) п", я + 5);      кейс 5 : printf("процесс (% d) п", я + 4);       кейс 4 : printf("процесс (% d) п", я + 3);       кейс 3 : printf("процесс (% d) п", я + 2);      кейс 2 : printf("процесс (% d) п", я + 1);   / * осталось два * /     кейс 1 : printf("процесс (% d) п", я);       / * остался только один для обработки * /      кейс 0 : ;                                 / * ничего не осталось * /  } }

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

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

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

1 двойной скалярное произведение = 0;2 за (int я = 0; я < 100; я++) {3   скалярное произведение += А[я]*B[я];4 }

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

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

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

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

 1     loop3: 2             l.d     $ f10, 0($5)       ; $ f10 ← A [i] 3             l.d     $ f12, 0($6)       ; $ f12 ← B [i] 4             mul.d   $ f10, $ f10, $ f12  ; $ f10 ← A [i] * B [i] 5             add.d   $ f8, $ f8, $ f10    ; $ f8 ← $ f8 + A [i] * B [i] 6             добавить    $5, $5, 8         ; увеличить указатель для A [i] на размер 7                                       ; двойного. 8             добавить    $6, $6, 8         ; увеличить указатель для B [i] на размер 9                                       ; двойного.10             добавить    $7, $7, -1        ; уменьшение количества циклов11     тестовое задание:12             БГТЗ    $7, loop3         ; Продолжить, если количество циклов> 0

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

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

 1     loop3: 2             l.d     $ f10, 0($5)         ; итерация со смещением 0 3             l.d     $ f12, 0($6) 4             mul.d   $ f10, $ f10, $ f12 5             add.d   $ f8, $ f8, $ f10 6  7             l.d     $ f10, 8($5)         ; итерация со смещением 8 8             l.d     $ f12, 8($6) 9             mul.d   $ f10, $ f10, $ f1210             add.d   $ f8, $ f8, $ f1011 12             l.d     $ f10, 16($5)        ; итерация со смещением 1613             l.d     $ f12, 16($6)14             mul.d   $ f10, $ f10, $ f1215             add.d   $ f8, $ f8, $ f1016 17             l.d     $ f10, 24($5)        ; итерация со смещением 2418             l.d     $ f12, 24($6)19             mul.d   $ f10, $ f10, $ f1220             add.d   $ f8, $ f8, $ f1021 22             добавить    $5, $5, 3223             добавить    $6, $6, 3224             добавить    $7, $7, -425     тестовое задание:26             БГТЗ    $7, loop3           ; Продолжить цикл, если $ 7> 0

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

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

  1. ^ Цо, Тед (22 августа 2000 г.). "Re: [PATCH] Re: Перенос драйверов ввода, нужно кое-что от вас". lkml.indiana.edu. Список рассылки ядра Linux. Получено 22 августа, 2014. У Джима Геттиса есть прекрасное объяснение этого эффекта на X-сервере. Оказывается, что с прогнозами ветвлений и относительной скоростью процессора по сравнению с памятью, меняющейся за последнее десятилетие, развертывание цикла в значительной степени бессмысленно. Фактически, удалив все экземпляры устройства Даффа из XFree86 4.0 сервер уменьшился в размере на _half_ _a_ _megabyte_ (!!!) и был быстрее загружен, потому что устранение всего этого лишнего кода означало, что X-сервер не так сильно загружал строки кэша.
  2. ^ Ульман, Джеффри Д .; Ахо, Альфред В. (1977). Принципы построения компилятора. Чтение, Массачусетс: Аддисон-Уэсли Паб. Co., стр.471–2. ISBN  0-201-10073-8.
  3. ^ Петерсен, В.П., Арбенз, П. (2004). Введение в параллельные вычисления. Издательство Оксфордского университета. п.10.CS1 maint: несколько имен: список авторов (ссылка на сайт)
  4. ^ Николау, Александру (1985). «Квантование петли: размотка для использования мелкозернистого параллелизма». Технический отчет Департамента компьютерных наук. Итака, штат Нью-Йорк: Корнельский университет. OCLC  14638257. Цитировать журнал требует | журнал = (Помогите)
  5. ^ Проверка модели с использованием SMT и теории списков
  6. ^ Туман, Агнер (2012-02-29). «Оптимизация подпрограмм на ассемблере» (PDF). Инженерный колледж Копенгагенского университета. п. 100. Получено 2012-09-22. 12.11 Раскрутка петли
  7. ^ Саркар, Вивек (2001). «Оптимизированное развертывание вложенных циклов». Международный журнал параллельного программирования. 29 (5): 545–581. Дои:10.1023 / А: 1012246031671. S2CID  3353104.
  8. ^ Адам Хорват «Раскрутка кода - до производительности еще далеко»
  9. ^ «Развертывание петли». Университет Минессота.

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

внешняя ссылка