- •3. Обращение к памяти
- •3.1. Краткий обзор
- •3.2. Не спекулятивные обращения к памяти
- •3.2.1. Сохранения в память
- •3.2.2. Загрузки из памяти
- •3.2.3. Подсказки предвыборки данных
- •3.3. Зависимости инструкций
- •3.3.1.1. Планирование инструкций и зависимости по управлению
- •3.3.2. Зависимости по данным
- •Раздел 3.4. Описывает определенные в архитектуре Itanium, особенности обращений к памяти, увеличивающие число зависимостей, которые могут быть удалены транслятором.
- •3.3.1. Зависимости по управлению
- •3.3.2.1. Основы зависимости по данным
- •3.3.2.2. Зависимость по данным в архитектуре Intel Itanium
- •3.3.2.3. Планирование инструкций и зависимости по данным
- •3.4. Использование спекулятивности в архитектуре Intel Itanium для преодоления зависимостей
- •3.4.1. Модель спекулятивности в архитектуре Intel Itanium
- •3.4.2. Использование спекуляции по данным в архитектуре Intel Itanium
- •3.4.2.1. Примеры предварительных загрузок
- •3.4.2.2. Пример кода восстановления
- •3.4.2.3. Краткий обзор терминологии
- •3.4.3. Использование спекуляции по управлению в архитектуре Intel Itanium
- •3.4.3.2. Пример спекуляции по управлению
- •3.4.3.3. Сливания, заливания и регистр unat
- •3.4.3.4. Краткий обзор терминологии
- •3.4.4. Комбинирование спекуляций по данным и управлению
- •3.5. Оптимизация обращений к памяти
- •3.5.1. Соображения о спекуляции
- •3.5.2. Взаимное влияние данных
- •3.5.3. Оптимизация размера кода
- •3.5.4. Использование постинкрементных загрузок и сохранений.
- •3.5.5. Оптимизация циклов.
- •3.5.6. Минимизация кода проверки
- •3.6. Итоги
3.3.2.1. Основы зависимости по данным
Следующие основные термины описывают зависимости по данным между инструкциями:
WAW (write after write) – запись после записи. Зависимость между двумя инструкциями, которые записывают один и тот же регистр или место в памяти.
WAR (write after read) – запись после чтения. Зависимость между двумя инструкциями, где инструкция читает регистр или место в памяти, в которое последующая инструкция делает запись.
RAW (read after write) – чтение после записи. Зависимость между двумя инструкциями, где инструкция записывает регистр или место в памяти, которое читается последующей инструкцией.
Неоднозначные зависимости памяти. Зависимости между загрузкой и сохранением, либо между двумя сохранениями, где невозможно определить обращаются ли эти инструкции к перекрывающимся областям. Неоднозначные обращения к памяти включают возможные зависимости WAW, WAR или RAW.
Независимые обращения к памяти. Обращения с помощью двух или более инструкций памяти, про которые не известно – имеют ли они конфликтные обращения к памяти.
3.3.2.2. Зависимость по данным в архитектуре Intel Itanium
Архитектура Itanium требует от программиста включать стопы между регистровыми зависимостями RAW и WAW, чтобы быть уверенном в корректном результатах кода. Например, в следующем коде инструкция add вычисляет значение в r4, которое необходимо для инструкции sub.
add r4=r5,r6 ;; // Группа инструкций 1
sub r7=r4,r9 // Группа инструкций 2
Стоп после инструкции add завершает одну группу инструкций, таким образом, инструкция sub может легально читать r4.
С другой стороны, выполнению типа Itanium требуется наблюдать за зависимостями, базирующимися на памяти в пределах группы инструкций. В одной группе инструкций, программа может содержать инструкции зависимые по данным от памяти, при этом аппаратные средства произведут те же результаты, которые получились бы, если бы инструкции выполнялись последовательно, в порядке программы. Приведенный ниже псевдокод, демонстрирует зависимость по памяти, которая будет наблюдаться аппаратно:
mov r16=1
mov r17=2 ;;
st8 [r15]=r16
st8 [r14]=r17;;
Если адрес в r14 равен адресу в r15, то однопроцессорная система гарантирует, что это место памяти будет содержать значение из r17 (2). Следующая зависимость RAW, тоже является легальной внутри одной группы инструкций, даже если программа не способна определить накладываются ли r1 и r2.
st8 [r1]=x
ld4 y=[r2]
3.3.2.3. Планирование инструкций и зависимости по данным
Правила зависимости являются достаточными для того, чтобы генерировать корректный код, но для генерации эффективного кода, компилятор должен принять во внимание время выполнения инструкций. Например, характерное выполнение имеет два такта для кэша данных первого уровня. В приведенном ниже примере, стоп обслуживает корректное упорядочивание, но использование r2 планируется только один такт после его загрузки:
add r7=r6,1 // Такт 0
add r13=r25,r27
cmp.eq p1,p2=r12,r23;;
add r11=r13,r29 // Такт 1
ld4 r2=[r3] ;;
sub r4=r2,r11 // Такт 3
Таким образом, задержка загрузки составит два такта, а инструкция sub будет простаивать до третьего такта. Чтобы избежать простоя компилятор может передвинуть загрузку ранее в очереди, так, чтобы машина могла выполнять полезную работу в каждом такте:
ld4 r2=[r3] // Такт 0
add r7=r6,1
add r13=r25,r27
cmp.eq p1,p2=r12,r23;;
add r11=r13,r29;;// Такт 1
sub r4=r2,r11 // Такт 2
В этом коде имеется достаточно независимых инструкций для перемещения загрузки ранее в очереди, чтобы улучшить использование функциональных устройств и уменьшить время выполнения на один такт.
Теперь предположим, что первоначальная последовательность кода содержала неоднозначную зависимость по памяти между инструкцией сохранения и инструкцией загрузки:
add r7=r6,1 // Такт 0
add r13=r25,r27
cmp.ne p1,p2=r12,r23;;
st4 [r29]=r13 // Такт 1
ld4 r2=[r3] ;;
sub r4=r2,r11 // Такт 3
В этом случае загрузка не может быть передвинута мимо сохранения из-за зависимости по памяти. Сохранения вызывают зависимости по данным, если они не могут не быть двусмысленными по отношению к загрузкам или другим сохранениям.
В отсутствие другой архитектурной поддержки, сохранения могут препятствовать перемещениям загрузок и зависимых от них инструкций. Следующие операторы языка C не могут быть переупорядочены, если не известно, что ptr1 и ptr2 указывают на независимые места памяти:
*ptr1 = 6;
x = *ptr2;
