Здравствуйие! Я хочу пройти курс Введение в принципы функционирования и применения современных мультиядерных архитектур (на примере Intel Xeon Phi), в презентации самостоятельной работы №1 указаны логин и пароль для доступ на кластер и выполнения самостоятельных работ, но войти по такой паре логин-пароль не получается. Как предполагается выполнение самосоятельных работ в этом курсе? |
Самостоятельная работа 5: Оптимизация вычислений в задаче матричного умножения. Оптимизация работы с памятью
Реализация блочного алгоритма умножения матриц
Один из способов улучшения доступа к памяти при реализации алгоритма умножения матриц – применение блочных алгоритмов. При использовании блочных алгоритмов повышается локальность доступа к памяти. Как правило, при повышении локальности доступа к памяти повышается эффективность ее использования за счет уменьшения количества кеш-промахов.
Использование блока квадратной формы
Реализуем блочный алгоритм. В первой версии будем использовать два предположения – блоки квадратные и порядок матрицы делится на размер блока без остатка.
Для реализации изменим прототип функции умножения (добавим в параметры размер блока):
// multBloc.h #ifndef _MULT_ #define _MULT_ #include "routine.h" //умножение матриц void mult(ELEMENT_TYPE * A, ELEMENT_TYPE * B, ELEMENT_TYPE * C, int n, int bSize); #endif
Частично изменим функцию main:
#include <stdio.h> #include "omp.h" #include "multBlock.h" int testThreadCount(); int main(int argc, char **argv) { double time_s, time_f; int mic_th; int n = 0, blockSize = 1; if(argc < 3) { printf("<exec> <n> <block Size>"); return -1; } n = atoi(argv[1]); blockSize = atoi(argv[2]); … time_s = omp_get_wtime( ); mult(A, B, C, n, blockSize); time_f = omp_get_wtime( ); … return 0; }
Реализуем функцию блочного умножения:
//singleBlock.cpp #include "mult.h" #include "assert.h" void mult(ELEMENT_TYPE * A, ELEMENT_TYPE * B, ELEMENT_TYPE * C, int n, int bSize) { ELEMENT_TYPE s, err; int i, j, k, ik, jk, kk; assert(n % bSize == 0); for(j = 0; j < n; j++ ) { for(i = 0; i < n; i++ ) { C[j * n + i] = 0; } } for(jk = 0; jk < n; jk+= bSize) for(kk = 0; kk < n; kk+= bSize) for(ik = 0; ik < n; ik+= bSize) for(j = 0; j < bSize; j++ ) for(k = 0; k < bSize; k++ ) #pragma simd for(i = 0; i < bSize; i++ ) C[(jk + j) * n + (ik + i)] += A[(jk + j) * n + (kk + k)] * B[(kk + k) * n + (ik + i)]; }
Скомпилируем и запустим код. На рис. 10.7 представлен результат выполнения блочного алгоритма на сопроцессоре Intel Xeon Phi.
увеличить изображение
Рис. 10.7. Результат выполнения блочного алгоритма на сопроцессоре Intel Xeon Phi.
В таблице 10.7 приведены времена работы программной реализации блочного алгоритма при разных размерах блока:
Размер блока: | 16 | 32 | 64 | 128 | jki | MKL seq. |
---|---|---|---|---|---|---|
N=1024 | 14,3 | 9,318 | 6,058 | 6,065 | 1,95 | 0,207 |
Из результатов экспериментов и рис. 10.8 видно, что блочный алгоритм вместо ускорения дал замедление.
В чем может быть причина?
Соберем отчет об оптимизации программы. Для этого можно использовать строку компиляции, представленную ниже:
icpc -mmic -mkl -openmp -opt-report=3 ./singleBlock.cpp ./mainBlock.cpp ./routine.cpp -osingleBlock
Далее приведена часть отчета об оптимизации:
… ./singleBlock.cpp(12:5-12:5):VEC:_Z4multPfS_S_ii: loop was not vectorized: loop was transformed to memset or memcpy ./singleBlock.cpp(10:3-10:3):VEC:_Z4multPfS_S_ii: loop was not vectorized: not inner loop ./singleBlock.cpp(29:13-29:13):VEC:_Z4multPfS_S_ii: LOOP WAS VECTORIZED loop skipped: multiversioned ./singleBlock.cpp(26:11-26:11):VEC:_Z4multPfS_S_ii: loop was not vectorized: not inner loop ./singleBlock.cpp(24:9-24:9):VEC:_Z4multPfS_S_ii: loop was not vectorized: not inner loop ./singleBlock.cpp(22:3-22:3):VEC:_Z4multPfS_S_ii: loop was not vectorized: not inner loop ./singleBlock.cpp(21:3-21:3):VEC:_Z4multPfS_S_ii: loop was not vectorized: not inner loop ./singleBlock.cpp(20:3-20:3):VEC:_Z4multPfS_S_ii: loop was not vectorized: not inner loop Total #of lines prefetched in _Z4multPfS_S_ii for loop at line 26=6 # of initial-value prefetches in _Z4multPfS_S_ii for loop at line 26=1 # of dynamic_mapped_array prefetches in _Z4multPfS_S_ii for loop at line 26=6, dist=6 Total #of lines prefetched in _Z4multPfS_S_ii for loop at line 29=8 # of initial-value prefetches in _Z4multPfS_S_ii for loop at line 29=6 # of dynamic_mapped_array prefetches in _Z4multPfS_S_ii for loop at line 29=8, dist=8 Total #of lines prefetched in _Z4multPfS_S_ii for loop at line 29=8 # of initial-value prefetches in _Z4multPfS_S_ii for loop at line 29=6 # of dynamic_mapped_array prefetches in _Z4multPfS_S_ii for loop at line 29=8, dist=8 Total #of lines prefetched in _Z4multPfS_S_ii for loop at line 29=4 # of initial-value prefetches in _Z4multPfS_S_ii for loop at line 29=2 # of dynamic_mapped_array prefetches in _Z4multPfS_S_ii for loop at line 29=4, dist=64
Из отчета видно, что компилятор добавил в код много вызовов программной предвыборки данных.
Для оптимизации работы с памятью компилятор использует программную предвыборку данных. При генерации кода компилятор заранее не знает, чему будут равны параметры, передаваемые в функцию. Из-за этого компилятор должен генерировать код, используя введенные в нем эвристики. Эвристики срабатывают не всегда. Например, компилятор мог предположить, что внутренний цикл имеет большую длину и соответствующим образам вставил предсказание загрузки данных в кэш-память. В нашем случае цикл имеет малый размер (сомнительно, что большие блоки больших размеров приведут к хорошей локальности при доступе к данным). Как следствие, в кэш-память загружается много не используемых данных, что негативно влияет на производительность.
Попробуем помочь компилятору, используя задание размера блока в виде константы. Заметим, что в результате подобных действий компилятор иногда может избавиться от короткого цикла, полностью развернув его и реализовав с использованием векторной арифметики.
const int bSize = 64;
Еще раз соберем отчет компилятора об оптимизации:
./singleBlock.cpp(15:5-15:5):VEC:_Z4multPfS_S_ii: loop was not vectorized: loop was transformed to memset or memcpy ./singleBlock.cpp(13:3-13:3):VEC:_Z4multPfS_S_ii: loop was not vectorized: not inner loop ./singleBlock.cpp(30:17-30:17):VEC:_Z4multPfS_S_ii: LOOP WAS VECTORIZED ./singleBlock.cpp(27:11-27:11):VEC:_Z4multPfS_S_ii: loop was not vectorized: not inner loop ./singleBlock.cpp(25:9-25:9):VEC:_Z4multPfS_S_ii: loop was not vectorized: not inner loop ./singleBlock.cpp(23:3-23:3):VEC:_Z4multPfS_S_ii: loop was not vectorized: not inner loop ./singleBlock.cpp(22:3-22:3):VEC:_Z4multPfS_S_ii: loop was not vectorized: not inner loop ./singleBlock.cpp(21:3-21:3):VEC:_Z4multPfS_S_ii: loop was not vectorized: not inner loop Estimate of max_trip_count of loop at line 27=128 Total #of lines prefetched in _Z4multPfS_S_ii for loop at line 27=2 # of dynamic_mapped_array prefetches in _Z4multPfS_S_ii for loop at line 27=2, dist=8 Estimate of max_trip_count of loop at line 30=4 Total #of lines prefetched in _Z4multPfS_S_ii for loop at line 30=4 # of initial-value prefetches in _Z4multPfS_S_ii for loop at line 30=6 # of dynamic_mapped_array prefetches in _Z4multPfS_S_ii for loop at line 30=4, dist=2 Estimate of max_trip_count of loop at line 30=4 Total #of lines prefetched in _Z4multPfS_S_ii for loop at line 30=4 # of initial-value prefetches in _Z4multPfS_S_ii for loop at line 30=6 # of dynamic_mapped_array prefetches in _Z4multPfS_S_ii for loop at line 30=4, dist=2 Using second-level distance 2 for prefetching dyn-map memory reference in stmt at line 30
Из отчета видно, что предсказания компилятора существенным образом поменялись. В частности предсказаний загрузки данных в кэш-память стало меньше, также изменились их параметры.
Попробуем замерить время вычислений при разных размерах блока, заданных при помощи константы и через параметр функции. В таблице 10.8 приведены результаты сравнения.
Размер блока: | 16 | 32 | 64 | 128 | jki | MKL seq. |
---|---|---|---|---|---|---|
N=1024\ Размер блока\ параметр | 14,3 | 9,318 | 6,058 | 6,065 | 1,95 | 0,207 |
N=1024\ Размер блока\ константа | 3,68 | 1,4 | 1,45 | 3,48 | 1,95 | 0,207 |

Рис. 10.9. Время работы алгоритма в зависимости от размера блока и типа его задания на Intel Xeon Phi
Из рис. 10.9 и таблицы 10.8 видно, что блочный алгоритм позволил уменьшить время вычислений. Время сократилось из-за уменьшения количества кэш-промахов.
Следует отметить, что вместо задания размера блока равного константе также можно попытаться использовать директивы компилятора, такие как #pragma loop_count, позволяющую компилятору подсказать информацию о характерных длинах цикла. Попробовать данные директивы предлагается самостоятельно.
В рассматриваемом примере приведены результаты только при четырех размерах блока. В качестве дополнительного задания предлагается найти оптимальный размер блока для рассматриваемых размеров матриц (1024, 2048 и 3072).