|
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
2006 г.
Использование параллелизма на уровне команд
А. Белеванцев, М. Кувырков, Д. Мельник., | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Исходная программа | Программа с инструкциями раннего выполнения | Исходная программа | Программа с инструкциями раннего выполнения | |
|---|---|---|---|---|
|
|
|
|
| |
| а) | б) | |||
Рис. 3. Примеры ассемблерного кода с командами раннего выполнения
| а) преодоление зависимостей по данным, |
| б) преодоление зависимостей по управлению |
Избыточная генерация инструкций раннего выполнения зачастую может не только не дать выигрыша в производительности, но и наоборот, привести к ухудшению производительности программы, т.к. в случае неудачного раннего выполнения необходимо будет выполнять загрузку из памяти повторно, или выполнять дополнительный код восстановления. Использование информации, полученной с помощью анализа указателей, во многих случаях дает возможность оценить целесообразность генерации инструкций раннего выполнения, что позволяет генерировать такие инструкции только в тех местах, где это действительно необходимо.
Инструкции раннего выполнения порождаются компилятором в процессе планирования. В этом разделе описывается, как нужно изменить планировщик, чтобы он мог поддерживать раннее выполнение. Предполагается, что планировщик может оперировать регионами из нескольких базовых блоков (иначе не появляется зависимостей по управлению и раннего выполнения для них). При описании того, как меняется собственно алгоритм планирования, мы предполагаем, что планировщик принадлежит к классу алгоритмов списочного планирования (list scheduling [4]).
Для моделирования раннего выполнения в планировщике инструкций введем понятие блока раннего выполнения. Некоторые инструкции могут порождать начало таких блоков (например, загрузки из памяти). Затем могут следовать несколько инструкций, использующих результат загрузки (первой инструкции блока). Блок завершается специальными инструкциями проверки (такими как ld.c и chk.s). Таким образом, блок раннего выполнения формируется из операций ранней загрузки и проверки, и может включать несколько использований результата ранней загрузки.
Алгоритм поддержки раннего выполнения в планировщике имеет своей целью корректное создание и наполнение блоков раннего выполнения наравне с планированием обычных инструкций. Для этого необходимо решить следующие подзадачи:
Далее мы подробно описываем решение каждой из этих задач.
Для отражения свойств инструкций и зависимостей, связанных с ранним выполнением, в структуры данных планировщика, представляющие инструкции и зависимости между ними, включены флаги раннего выполнения (см. таблицу 1). Наличие флага раннего выполнения для зависимости означает, что для преодоления данной зависимости можно использовать раннее выполнение. Аналогично, такой флаг для инструкции означает, что она может быть запланирована альтернативным способом - с использованием раннего выполнения. Также специальными флагами помечаются инструкции, которые более предпочтительны для раннего выполнения, либо, наоборот, не должны в нем участвовать.
Таблица 1. Флаги раннего выполнения
| Флаг | Зависимость может быть устранена с помощью: | Инструкция может быть: |
|---|---|---|
| BEGIN_DATA | ранняя загрузка с помощью ld.a | ld.a |
| BEGIN_CONTROL | ранняя загрузка с помощью ld.s | ld.s |
| BE_IN_DATA
BE_IN_CONTROL | использование результата ранней загрузки | использование результата ранней загрузки |
| FINISH_DATA | - | ld.c |
| FINISH_CONTROL | - | chk.s |
| HARD_DEP | невозможно устранить | ранняя загрузка не может быть использована |
| WEAK_DEP | ранняя загрузка предпочтительна | ранняя загрузка предпочтительна |
Инициализация флагов происходит перед началом планирования, когда работает анализ зависимостей по данным. Все так называемые истинные зависимости по памяти (команда, читающая данные из памяти, зависит от ранее выполняемой команды записи этих данных) помечаются анализом зависимостей флагом BEGIN_DATA. Некоторые зависимости при этом помечаются флагом HARD_DEP (например, зависимости, возникающие при волатильных обращениях в память). Флаг зависимости по управлению BEGIN_CONTROL, BE_IN- и FINISH- флаги, а также флаги раннего выполнения для инструкций устанавливаются в процессе планирования при анализе их зависимостей.
Планировщиком семейства list scheduling непосредственно для планирования используется список готовых инструкций. Инструкция может быть помещена в этот список при начале планирования, либо после того, как запланирована предыдущая инструкция и удовлетворены зависимости по данным. Из списка выбирается (обычно руководствуясь набором эвристик) наилучшая для планирования в данный момент инструкция, которая выдается на планирование и удаляется из списка, а зависимые от нее инструкции добавляются в список. Процесс повторяется до тех пор, пока не будут запланированы все инструкции.
С точки зрения планировщика, все истинные зависимости по памяти могут быть потенциально устранены с помощью раннего выполнения по данным. Дополнительное ограничение накладывает архитектура машины, которая может поддерживать раннюю выдачу лишь некоторых инструкций (в Itanium это команды загрузки из памяти). За проверку этого ограничения отвечает машинно-зависимая часть компилятора. Аналогично, любая инструкция из последующих базовых блоков может быть выполнена в планируемом блоке с помощью раннего выполнения по управлению, если это поддерживается целевой машиной. Это ограничение проверяется при каждом перемещении инструкции в список для планирования и далее в этом разделе не упоминается.
BEGIN_DATA либо BE_IN_DATA. Такая инструкция при помещении в список также помечается одним из этих флагов.
Если же инструкция находится в другом базовом блоке, то возможны три случая. В первом случае у инструкции нет зависимостей по данным, и она не может возбудить исключение - перемещение такой инструкции не нарушает корректность программы, и она сразу помещается в список. Во втором случае инструкция может создать исключение, но не имеет зависимостей. Эта инструкция может быть помещена в список для раннего выполнения по управлению, если вероятность выполнения ее базового блока относительно текущего высока, и в этом случае она помечается флагом BEGIN_CONTROL. В третьем случае у инструкции есть зависимости по данным. Если все такие зависимости помечены флагами BEGIN_DATA либо BE_IN_DATA, тогда, аналогично двум предыдущим случаям, в зависимости от того, может или нет инструкция возбудить исключение, она может быть помещена в список с флагом (либо без флага) BEGIN_CONTROL и с флагами, соответствующими флагам ее зависимостей. Если же зависимости инструкции устранить невозможно (флаг HARD_DEP), то она не может быть помещена в список планирования.
BEGIN_* (загрузка из памяти в случае Itanium) разбивается на две части: инструкцию раннего выполнения и инструкцию проверки. Все зависимости исходной инструкции (как прямые, так и обратные), переносятся на инструкцию проверки, а также добавляется зависимость между инструкциями раннего выполнения и проверки. При этом обратные зависимости с флагом HARD_DEP изменяются на зависимости BE_IN_*. Инструкция раннего выполнения планируется на текущем цикле, а инструкция проверки помечается как последняя инструкция блока раннего выполнения (FINISH_*), и планируется позже, как обычная инструкция. Кроме того, при планировании инструкций, создающих новый блок раннего выполнения, также создается новый блок, который будет содержать код восстановления, и в него помещается копия спланированной инструкции. Для данной копии создается зависимость от инструкции проверки типа HARD_DEP, которая обеспечивает планирование кода восстановления после проверочной инструкции.
При выдаче инструкции, содержащейся внутри блока раннего выполнения (помеченной флагами BE_IN_*), аналогично ее зависимости также изменяют свой тип с BEGIN_* на BE_IN_*, и копия инструкции помещается в уже созданный блок восстановления. При этом BE_IN_* зависимости инструкции, которые были устранены при планировании, перемещаются на копию инструкции, чтобы указать планировщику зависимость копии инструкции от уже содержащихся в блоке восстановления инструкций.
При выдаче инструкции проверки, завершающей блок раннего выполнения, соответствующий блок восстановления закрывается и добавляется к текущему региону для последующего планирования аналогично обычным базовым блокам. Если блок выполнения состоит из одной инструкции, то возможна ситуация, когда задачу восстановления выполнит сама инструкция проверки (в случае Itanium это возможно для ld.c). Тогда блок восстановления уничтожается.
При разработке алгоритма раннего выполнения мы ориентировались на мультиплатформенный компилятор, подобный GCC. Для этого машинно-зависимые части поддержки раннего выполнения были выделены отдельно. Если необходимо реализовать поддержку для определенной платформы, то модуль компилятора, реализующий кодогенерацию для этой платформы, должен предоставить следующие возможности (реализованные в виде процедур):
ld.s или ld.a;
Кроме того, для всех типов инструкций раннего выполнения необходимо задать их вид в ассемблере целевой машины для кодогенератора, а также время выполнения (латентность) и занимаемые функциональные устройства для того, чтобы планировщик мог оценивать состояние конвейера целевой машины в процессе планирования.
Данные анализа указателей могут значительно повысить эффективность планирования инструкций с поддержкой раннего выполнения, указывая планировщику, в каких случаях генерация инструкций раннего выполнения является наиболее эффективной. На основе данных анализа указателей с помощью приведенных ниже эвристик оценивается степень истинной зависимости между инструкциями. Будем говорить, что между двумя инструкциями существует слабая зависимость, если между ними с помощью консервативного статического анализа диагностируется истинная зависимость по данным, но фактически вероятность возникновения такой зависимости мала. Аналогично, будем считать, что сильной зависимостью между двумя инструкциями является такая истинная зависимость по данным, которая с достаточно большой вероятностью имеет место и во время выполнения программы.
Для определения степени зависимости вычисляется эвристическая оценка, показывающая вероятность ее существования с точки зрения статического анализа. Используются следующие эвристики:
s.a или p->b);
p и q имеют видp = arg1 + offset1, q = arg2 + offset2,arg1 и arg2 - различные аргументы функции.
Аналогично, зависимость является сильной, и мы не должны пытаться «разорвать» зависимость по данным, если множества points-to соответствующих указателей пересекаются. В этом случае, как было установлено экспериментально, высока вероятность того, что раннее выполнение окажется неудачным.
Данный подход был реализован в компиляторе GCC на основе серии 4.х (в настоящий момент еще не вышедшей). Логика планирования инструкций раннего выполнения и расширение структур данных планировщика были реализованы так, как описано в разделе 3. В кодогенераторе GCC для процессоров Itanium были описаны ассемблерные формы инструкций раннего выполнения. Кроме того, был исправлен ряд недочетов и сделано несколько улучшений планировщика, не связанных непосредственно с основным алгоритмом:
<точка планирования> add r3 = r3, r4 st [r6] = r4 ld r4 = [r5]
Загрузка в регистр r4 не может быть перемещена для раннего выполнения в текущую точку планирования, поскольку такое перемещение нарушит обратную зависимость (anti-dependence) между инструкциями ld и add. Между тем, эта зависимость может быть опущена из-за наличия прямой (истинной) зависимости у инструкции ld. Мы исправили анализ зависимостей так, чтобы сохранялись все типы зависимостей;
Мы провели тестирование раннего выполнения на наборе тестов SPEC 2000 [5]. Использовались серверы HP rx1600 с двумя процессорами Intel Itanium 2 1.8 ГГЦ и 2 ГБ оперативной памяти. В таблице 2 приведены результаты тестирования для пакета SPEC FP c уровнем оптимизации -O3. Для сравнения приведены также данные ускорений, получаемые при включении отдельных оптимизаций. При уровне оптимизации -O3, помимо раннего выполнения, работает также и широкий набор стандартных оптимизаций компилятора GCC.
Результаты тестирования показывают, что ранее выполнение наиболее полезно для вычислительных задач, где применение этой техники может дать большое ускорение (до 20% и выше). Для целочисленных задач применение техники должно быть более консервативным, и стандартного анализа указателей (компилятора GCC) может не хватать. Вообще говоря, чем более консервативные настройки раннего выполнения, тем меньше ускорение для отдельных тестов, но при этом и меньше тестов, показывающих худшие результаты.
Таблица 2. Результаты тестирования на SPEC FP 2000.
| Тесты | Только по данным | Только по управ- лению | По данным и по управ- лению | Только анализ указа- телей | Все вместе | Оптимизация -O3 |
|---|---|---|---|---|---|---|
| 168.wupwise | 0,71% | 1,43% | 1,43% | 1,66% | 0,95% | -0,47% |
| 171.swim | -0,30% | -0,30% | 0,30% | -0,44% | -0,15% | -0,15% |
| 172.mgrid | 0,00% | 0,00% | 0,30% | 4,79% | 5,09% | -4,84% |
| 173.applu | 0,24% | 0,00% | 1,18% | -0,71% | -0,24% | 0,95% |
| 177.mesa | -1,09% | 0,00% | 2,89% | 0,14% | 0,82% | 1,73% |
| 178.galgel | 2,51% | -5,75% | 2,51% | -5,92% | -3,41% | 8,76% |
| 179.art | 1,05% | 0,06% | -0,17% | -0,06% | 0,58% | -0,23% |
| 183.equake | -0,90% | -0,23% | -1,13% | -0,23% | -0,45% | -2,24% |
| 187.facerec | 0,19% | 0,00% | -1,12% | -3,16% | -2,99% | 2,87% |
| 188.ammp | 18,84% | 0,15% | 18,84% | -1,52% | 16,87% | 20,68% |
| 189.lucas | 0,12% | -0,12% | -0,36% | -0,12% | 0,00% | 0,00% |
| 191.fma3d | 0,73% | -0,36% | 0,36% | 0,73% | 2,93% | -0,72% |
| 200.sixtrack | 2,43% | 0,00% | 2,08% | -1,04% | 1,04% | 3,16% |
| 301.apsi | 1,12% | -0,67% | 0,45% | -4,45% | -3,13% | 5,57% |
| SPEC FP 2000 | 1,71% | -0,57% | 1,89% | -0,76% | 1,14% | 2,46% |
В этой статье мы описали проблемы, возникающие при компиляции для архитектур с явно выраженным параллелизмом на уровне команд, на примере задачи поддержки раннего выполнения для Intel Itanium. Разработанный нами алгоритм реализован в компиляторе GCC и протестирован на пакете SPEC CPU 2000. Алгоритм показывает ускорение примерно в 2,5% на пакете вычислительных программ SPEC FP 2000, причем отдельное ускорение достигает 20% (для теста ammp).
В наших дальнейших планах тонкая настройка алгоритма раннего выполнения, а также тестирование этого алгоритма с улучшенным анализом указателей, разработанным нами в рамках предыдущих исследований. Мы планируем включить нашу реализацию алгоритма раннего выполнения в компилятор GCC версии 4.2.
| 1.обратно | EPIC Technology Whitepaper. (файл в формате pdf) |
| 2.обратно | GNU Compiler Collection. |
| 3.обратно | Intel(R) Itanium(R) Architecture Software Developer's Manual. |
| 4.обратно | S. Muchnick. Advanced compiler design and implementation. Morgan Kaufmann, 3rd ed., 1997. |
| 5.обратно | SPEC CPU benchmark. |
|
CITForum © 1997–2025