Скачиваний:
56
Добавлен:
16.04.2013
Размер:
123.39 Кб
Скачать

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 «Программная конвейерная обработка и поддержка циклов».