Здравствуйие! Я хочу пройти курс Введение в принципы функционирования и применения современных мультиядерных архитектур (на примере Intel Xeon Phi), в презентации самостоятельной работы №1 указаны логин и пароль для доступ на кластер и выполнения самостоятельных работ, но войти по такой паре логин-пароль не получается. Как предполагается выполнение самосоятельных работ в этом курсе? |
Лекция 5: Элементы оптимизации прикладных программ для Intel Xeon Phi. Intel C/C++ Compiler
Технология Array Notation в Intel Cilk Plus
Другой способ векторизации кода – использование расширения Intel Cilk Plus для языков программирования C/C++ в части векторизации кода. Для нашего примера лучше всего подойдет применение специальной технологии Array Notation, с использованием которой код можно переписать так (обратите внимание на отсутствие цикла):
A[:] = B[:] + C[:];
Использование специальной технологии Array Notation является мощным инструментом для векторизации и значительно более сложных участков кода. Остановимся на ней подробнее.
Во-первых, обсудим синтаксис этой нотации:
- с помощью выражения A[:] задается весь массив A (размер массива определяется на этапе компиляции, а значит должен быть константным; принципы работы с динамическими массивами обсудим далее);
- выражение A[start_index : length] задает отрезок массива, начиная со start_index длиной length ( рис. 5.6);
- выражение A[start_index : length : stride] говорит о том, что мы хотим использовать каждый stride элемент массива, начиная со start_index. Количество таких элементов должно быть равно length ( рис. 5.7).
Рис. 5.6. Задание непрерывного отрезка массива с помощью технологии Array Notation расширения Intel Cilk Plus
Рис. 5.7. Задание множества элементов массива с помощью технологии Array Notation расширения Intel Cilk Plus
Многомерные массивы также поддерживаются.
Во-вторых, приведем список действий, которые можно применять к массивам в этой нотации:
- Возможно использование операторов языков C/C++:
d[:] = a[:] + (b[:]*c[:]);
- Возможна передача массивов в качестве аргументов функции. При этом вызов функции осуществляется для каждого заданного элемента массива:
b[:] = func(a[:]);
- Поддерживается операция редукции для сложения, минимума, максимума и т.п.:
sum = __sec_reduce_add(a[:]);
- Поддерживаются условные операторы if-then-else:
if (mask[:]) { a[:] = b[:]; }
- Поддерживаются операции типа scatter/gather, с помощью которых можно собрать определенные элементы одного массива в другой (собрать разрозненные элементы в один непрерывный массив), и наоборот:
c[:] = a[b[:]]; //gather a[b[:]] = c[:]; //scatter
- Поддерживаются операции сдвига. Операция shift сдвигает элементы массива на shift_val позиций влево/вправо, освободившиеся элементы заполняются значением fill_val. Операция rotate обеспечивает циклический сдвиг элементов влево/вправо на rotate_val позиций. Результат работы этих функций записывается в новый массив:
b[:] = __sec_shift_right(a[:], shift_val, fill_val); b[:] = __sec_shift_left(a[:], shift_val, fill_val); b[:] = __sec_rotate_right(a[:], rotate_val); b[:] = __sec_rotate_left(a[:], rotate_val);
И наконец, опишем принципы работы этого механизма:
- Размер массива должен быть известен на стадии компиляции. Если используется динамический массив, то при использовании данной нотации необходимо явно указывать начальную позицию (start_index) и длину (length) отрезка массива.
- В случае если векторизация кода невозможна, будет сгенерирован обычный цикл.
- Оптимальный векторный код будет получен только в случае работы с выровненными данными. Под выравниванием данных здесь понимается размещение массива элементов в памяти таким образом, чтобы адрес начала массива был кратен 64 байтам. Если данные не выровнены или количество элементов массива не кратно размеру регистра, то векторизация будет выполнена только над частью данных, остальные элементы будут вычисляться с помощью цикла.
- Требуется соответствие рангов и длин массивов в рамках одной операции:
a[0:5] = b[0:6]; // No. Size mismatch. a[0:5][0:4] = b[0:5]; // No. Rank mismatch. a[0:5] = b[0:5][0:5]; // No. No 2D->1D reduction. a[0:4] = 5; // OK. 4 elements of A filled w/ 5. a[0:4] = b[i]; // OK. Fill with scalar b[i]. a[10][0:4] = b[1:4]; // OK. Both are 1D sections. b[i] = a[0:4]; // No. Use reduction intrinsic.
Приведем еще один пример использования специальной технологии Array Notation. Выполняется скалярное произведение векторов. Скалярный код выглядит так:
float dot_product(unsigned int size, float *A, float *B) { int i; float dp=0.0f; for (i=0; i<size; i++) { dp += A[i] * B[i]; } return dp; }
С использованием Intel Cilk Plus его можно написать так:
float dot_product(unsigned int size, float A[size], float B[size]) { return __sec_reduce_add(A[:] * B[:]); }
В обеих версиях используются статические массивы размера size. При этом вторая версия кода будет работать с использованием векторных расширений процессора или сопроцессора.
Для динамических массивов идея использования этого метода сводится к следующему. Пишется функция векторной обработки блока элементов с константным размером блока. Далее исходный массив разбивается на блоки заданной длины, затем в цикле для каждого блока вызывается написанная ранее функция. Обработка блоков может осуществляться параллельно несколькими потоками:
#include <cilk\cilk.h> void saxpy_vec(int m, float a, float x[m], float y[m]) { y[0:m] += a*x[0:m]; } void main(void) { int n = 2048; const int m = 256; float* a = new float[n]; float* b = new float[n]; cilk_for (int i = 0; i < n; i += m) { saxpy_vec(m, 2.0, &a[i], &b[i]); } }