Для более простого написания векторизированного кода, расширения Cilk Plus включают в себя нотацию массивов, которая выходит за пределы стадарта С++. Данный мощный механизм применяется во многих сферах и является ценным инструментом для научных программистов и инженеров.
Нотации массивов являются добавлениями к компилятору Intel, которые не входят в стандарт С++. Они используются при векторизации и многоядерном параллелизме для обеспечения высоко паралелльного и оптимизированного кода.
Проведя большое количество времени с научными программистами и инженерами, я понял, что многие из них смогли бы использовать нотации массивов в научном моделировании, но они об этом не знают. Так что я рассмотрю несколько их научных применений.
Линейная алгебра
Физика основывается на линейной алгебре. Многие научные проблемы могут быть смоделированы с помощью концептов векторов и матриц. Курсы элементарной физики начинаются с изучения взаимодействия векторов.
В более общем виде, вектор представляет собой набор чисел, расположенных в определенном порядке. В физике с помощью вектора изображается сила, ее направленость и её модуль (который называется значением). Когда вы давите на что-либо, вы прикладываете силу, которая заставляет объект двигаться в определенном направлении. Сила также имеет свое значение. Вы можете изобразить силу графически с помощью стрелки, указывающей в определенном направлении, причём длина данной стрелки будет определять значение силы. Если начало вектора лежит в трехмерном пространстве, то его конец будет иметь координату, состоящую из трех точек. Эти три цифры могут использоваться для полного представления вектора. Таким образом, сила может иметь вектор с такими цифрами:
{ 1.0, 2.0, 3.0 }
Если силы действуют одновременно, чтобы получить результирующую силу, нужно сложить компоненты. Так что если у вас есть вторая сила, которая выглядит следующим образом:
{ 7.0, 9.0, 11.0 }
И эти силы действуют на объект одновременно, конечная результирующая сила будет являться простой суммой данных компонентов:
{ 1.0 + 7.0, 2.0 + 9.0, 3.0 + 11.0 }
Используя нотацию массива, мы легко можем сложить эти значения и получить новый вектор, совершив лишь одну операцию. Вот часть кода, которая именно это и делает:
float A[3] = { 1.0, 2.0, 3.0 }; float B[3] = { 7.0, 9.0, 11.0 }; float C[3]; C[:] = A[:] + B[:]; printf("{ %.2f, %.2f, %.2f }", C[0], C[1], C[2]);
Четвёртая строка - это ключ. Она складывает все элементы массивов, используя векторизацию, где это возможно. (Мы также должны убедиться в том, что данные выравнены, о чем я расскажу в следующей записи. Обратите внимание на то, что я также использую довольно удобный оператор printf, чтобы можно было сразу отформатировать итог, а не соединять различные вставки cout потоков).
Скалярные произведения
Ещё одной распространенной проблемой в инженерии является понятице скалярного произведения. С их помощью мы можем прийти к более сложным операциям над матрицами, которые также рассмотрим позже. А сейчас я только скажу, что скалярное произведение - это просто действие, когда два соответствующих компонента двух векторов перемножаются, и в результате получается новый вектор, а затем компоненты этого вектора суммируются. Например, если взять наши предыдущие два вектора, сначала мы перемножаем соответствующие значения:
{ 1.0 x 7.0, 2.0 x 9.0, 3.0 x 11.0 }
В результате чего получился такой вектор:
{ 7.0, 18.0, 33.0 }
Затем мы складываем эти значения:
7.0 + 18.0 + 33.0 = 58.0
Кажется, что такие операции можно легко выполнить с помощью Cilk Plus. Но это не так: стандартная библиотека Cilk Plus содержит функцию __sec_reduce_add, которая использует редукцию для параллельного сложения чисел. Мы можем вставить в неё результат операции над массивом:
float x = __sec_reduce_add(A[:] * B[:]);
где A и B имеют значения, определенные нами в вышеуказанном коде. Заметьте, что я делаю всё это в один приём. Операция
A[:] * B[:]
создаёт вектор, компоненты которого являются произведениями соответствующих компонентов в A и B. Далее я вставляю этот вектор в __sec_reduc_add, которая производит сложение всех членов вектора. И вот так мы получаем наш конечный результат. Вот код, который это делает:
float A[3] = { 1.0, 2.0, 3.0 }; float B[3] = { 7.0, 9.0, 11.0 }; float x = __sec_reduce_add(A[:] * B[:]); printf("%.2f", x);
Вывод
Нотация массива - очень мощный и бесспорно полезный инструмент в приложениях для научных и инженерных исследований.
Джефф КОГСВЕЛЛ (Jeff Cogswell)