- •6. Приложения с плавающей точкой
- •6.1. Краткий обзор
- •6.2. Ограничения эффективности приложений с плавающей точкой.
- •6.2.1. Время ожидания выполнения
- •6.2.2. Пропускная способность выполнения
- •6.2.3. Время задержки при обращении к памяти
- •6.2.4. Пропускная способность памяти
- •6.3. Особенности с плавающей точкой в архитектуре IntelItanium
- •6.3.1. Большой и широкий набор регистров с плавающей точкой
- •6.3.1.1. Примечания относительно точности с плавающей точкой.
- •6.3.2. Инструкции умножения со сложением
- •6.3.3 Программная последовательность деление/вычисление квадратного корня
- •6.3.3.1. Удвоенная точность – деление
- •6.3.3.2. Удвоенная точность – вычисление квадратного корня
- •6.3.4. Вычислительные модели
- •6.3.5. Множество полей состояния.
- •6.3.6. Другие свойства
- •6.3.6.1. Поддержка экранирования операнда.
- •6.3.6.2. Min/Max/Amin/aMax
- •6.3.6.3. Преобразования между целыми и плавающими числами
- •6.3.6.4. Обработка подполей с плавающей точкой
- •6.3.7. Управление доступом к памяти
- •6.3.7.1. Инструкции парной загрузки
- •6.3.7.2. Предвыборка данных
- •6.3.7.3. Управление распределением
- •6.4. Итоги
6.2.3. Время задержки при обращении к памяти
Пока что не одинаково время цикла у процессора и у памяти, и это создает общую проблему задержки памяти для большинства кодов, существует несколько специальных условий в кодах с плавающей точкой, которые усиливают упомянутое воздействие.
Одно такое условие – это использование косвенной адресации. Коды сборки/рассеивания, в особенности, нижеприведенный код умножения обычных и разряженных матричных векторов являются хорошими примерами.
DO 1 ROW = 1, N
R[ROW] = 0.0d0
DO 1 I = ROWEND(ROW-1)+1, ROWEND(ROW)
1 R[ROW] = R[ROW] + A[I] * X[COL[I]]
Время ожидания доступа к памяти COL[I] представляется таким, какое нужно для его использования в качестве индекса вектора Х. Доступ к элементу Х, вычисление произведения, суммирование произведения вR[ROW] – все это зависит от времени ожидания доступа кCOL[I].
Другое, обычное для кодов с плавающей точкой, условие – это усиление воздействия времени ожидания памяти из-за присутствия неоднозначных зависимостей по памяти. Рассмотрим выдержки из ядра градиента сопряжения Холецкого (Cholesky conjugategradient), опять же из набораLFK.
II = n
IPNTP = 0
222 IPNT = IPNTP
IPNTP = IPNTP + II
II = II/2
I = IPNTP + 1
cdir$ ivdep
DO 2 K = IPNT+2, IPNTP, 2
I = I+1
2 X[I]= X[K] - V[K] * X[K-1] - V[K-1] * X[K+1]
IF (II .GT. 1) GO TO 222
В DO-цикле имеется модификацияXс индексомI, при использовании Х с индексамиK,K+1,K-1. Поскольку транслятору трудно установить, нет ли перекрытия адресов, то загрузкиX[K],X[K+1],X[K-1] для следующей итерации не могут планироваться до тех пор, пока в текущей итерации не произойдет сохраненияX[I]. Это приводит к времени ожидания доступа к памяти этих операндов.
6.2.4. Пропускная способность памяти
Циклы с плавающей точкой, часто ограничены скоростью, с которой машина может поставлять операнды для вычислений. Типичным примером является ядро DAXPYиз библиотекиBLAS1:
DO 1 I = 1, N
1 Y[I] = Y[I] + A * X[I]
Вычисление требует для каждой операции умножения и сложения, загрузки двух операндов (X[I]иY[I]) и сохранения одного результата (Y[I]). Если массивы данных (XиY) не находятся в кэше, то эффективность этого цикла на самых современных микропроцессорах будет ограничена доступной пропускной способностью (bandwidth) памяти машины.
6.3. Особенности с плавающей точкой в архитектуре IntelItanium
В этом разделе освещаются архитектурные особенности, которые уменьшают воздействие ограничителей эффективности, описанных в иллюстративных примерах раздела 6.2.
6.3.1. Большой и широкий набор регистров с плавающей точкой
Поскольку уменьшается время машинного такта, то вообще-то увеличивается время ожидания выполнения устройств (в тактах). Поскольку увеличивается время ожидания, то также увеличивается давление на регистры в потоке многократных операций. Кроме того, поскольку добавляются многочисленные исполнительные устройства, то давление на регистры увеличивается еще больше из-за того, что в любой момент запускается множество инструкций.
Архитектура Itaniumобеспечивает 128 непосредственно адресуемых регистров с плавающей точкой, позволяющих многократное применение данных и уменьшающих количество операций загрузки/сохранения, вызванных недостаточным количеством регистров. Это сокращение загрузок и сохранений может увеличивать эффективность, путем замены вычислений ограниченных операциями с памятью (MOP–memoryoperation), на вычисления ограниченные операциями с плавающей точкой (FLOPfloatingpointoperation). Рассмотрим код умножения плотной матрицы:
DO 1 i = 1, N
DO 1 j = 1, P
DO 1 k = 1, M
1 C[i,j] = C[i,j] + A[i,k]*B[k,j]
Во внутреннем цикле (k) требуются две загрузки для каждой операции умножения и сложения. Поэтому отношениеMOP:FLOPравно 1:1.
L1: ldfd f5 = [r5], 8 // Загрузка A[i,k]
ldfd f6 = [r6], 8 // Загрузка B[k,j]
fma.d.s0 f7 = f5, f6, f7 // *,+ к C[i,j]
br.cloop L1
Здесь требуются три регистра – для хранения операндов (f5, f6) и для аккумулятора (f7). Поскольку, значениеA[i,k] повторно используется для различныхB[k,j] при измененииj, а значениеB[k,j] повторно используется для различныхA[i,k] при измененииi, то структура вычислений может быть изменена так:
DO 1 i = 1, N, 2
DO 1 j = 1, P, 2
DO 1 k = 1, M
C[i ,j ] = C[i ,j ] + A[i ,k]*B[k,j ]
C[i+1,j ] = C[i+1,j ] + A[i+1,k]*B[k,j ]
C[i ,j+1] = C[i ,j+1] + A[i ,k]*B[k,j+1]
1 C[i+1,j+1] = C[i+1,j+1] + A[i+1,k]*B[k,j+1]
Теперь, для каждых четырех загрузок могут быть выполнены четыре умножения и сложения, таким образом, отношение MOP:FLOPменяется на 1:2. Однако теперь требуется 8 регистров – 4 для аккумуляторов и 4 для операндов.
add r6 = r5, 8
add r8 = r7, 8
L1: ldfd f5 = [r5], 16 // Загрузка A[i,k]
ldfd f6 = [r6], 16 // Загрузка A[i+1,k]
ldfd f7 = [r7], 16 // Загрузка B[k,j]
ldfd f8 = [r8], 16 // Загрузка B[k,j+1]
fma.s0 f9 = f5, f7, f9 // *,+ в C[i,j]
fma.s0 f10 = f6, f7, f10 // *,+ в C[i+1,j]
fma.s0 f11 = f5, f8, f11 // *,+ в C[i,j+1]
fma.s0 f12 = f6, f8, f12 // *,+ в C[i+1,j+1]
br.cloop L1
При 128 доступных регистрах, внешние циклы (по iиj) могли бы быть развернуты в 8 раз каждый так, чтобы 64 умножения и сложения могли бы быть выполнены при загрузке только 16 операндов.
Файл регистров с плавающей точкой делится на два региона: статический регион (f0-f31) и ротируемый регион (f32-f127). Ротация регистров обеспечивает автоматическое переименование, требуемое для создания компактного кода ядра с программной конвейерной обработкой. Ротация регистров также позволяет планировать программный конвейерный код с интервалом инициализации меньшим, чем время ожидания самой длинной операции. Например, рассмотрим простой цикл сложения векторов:
DO 1 i = 1, N
1 A[i] = B[i] + C[i]
Основой внутреннего цикла является:
L1: ldf f5 = [r5], 8 // Загрузка B[i]
ldf f6 = [r6], 8 // Загрузка C[i]
fadd f7 = f5, f6 // Сложение операндов
stf [r7]= f7, 8 // Сохранение A[i]
br.cloop L1
Если мы положим, что минимальное время выполнения загрузки с плавающей точкой равно 9 тактам и одновременно могут быть запущены 2 операции с памятью, то этот цикл должен быть развернут, по крайней мере, в шесть раз, если нет ротации регистров.
add r8 = r7, 8
L1: (p18) stf [r7] = f25, 16 // Такты 17,26...
(p18) stf [r8] = f26, 16 // Такты 17,26...
(p17) fadd f25 = f5, f15 // Такты 8,17,26...
(p16) ldf f5 = [r5], 8 // Такты 0,9,18...
(p16) ldf f15 = [r6], 8 // Такты 0,9,18...
(p17) fadd f26 = f6, f16;; // Такты 9,18,27 ...
(p16) ldf f6 = [r5], 8 // Такты 1,10,19 ...
(p16) ldf f16 = [r6], 8 // Такты 1,10,19 ...
(p18) stf [r7] = f27, 16 // Такты 20,29 ...
(p18) stf [r8] = f28, 16 // Такты 20,29 ...
(p17) fadd f27 = f7, f17 ;; // Такты 11,20 ...
(p16) ldf f7 = [r5], 8 // Такты 3,12,21 ...
(p16) ldf f17 = [r6], 8 // Такты 3,12,21 ...
(p17) fadd f28 = f8, f18 ;; // Такты 12,21 ...
(p16) ldf f8 = [r5], 8 // Такты 4,13,22 ...
(p16) ldf f18 = [r6], 8 // Такты 4,13,22 ...
(p18) stf [r7] = f29, 16 // Такты 23,32 ...
(p18) stf [r8] = f30, 16 // Такты 23,32 ...
(p16) fadd f29 = f9, f19 ;; // Такты 14,23 ...
(p16) ldf f9 = [r5], 8 // Такты 6,15,24 ...
(p16) ldf f19 = [r6], 8 // Такты 6,15,24 ...
(p16) fadd f30 = f10, f20 ;; // Такты 15,24 ...
(p16) ldf f10 = [r5], 8 // Такты 7,16,25 ...
(p16) ldf f20 = [r6], 8 // Такты 7,16,25 ...
br.ctop L1 ;;
Однако, при ротации регистров, тот же самый цикл может быть спланирован с интервалом инициализации 2 такта, без развертывания (и 1.5 такта при развертывании вдвое):
L1: (p24) stf [r7] = f57, 8 // Такты 15,17...
(p21) fadd f57 = f37, f47 // Такты 9,11,13...
(p16) ldf f32 = [r5], 8 // Такты 0,2,4,6...
(p16) ldf f42 = [r6], 8 // Такты 0,2,4,6...
br.ctop L1;;
Таким образом, это часто выгодно для модульного планирования с дальнейшим разворачиванием (если требуется). О деталях относительно того, как переписать циклы, использующие это преобразование, см. главу 5 «Программная конвейерная обработка и поддержка циклов».