|
Здравствуйие! Я хочу пройти курс Введение в принципы функционирования и применения современных мультиядерных архитектур (на примере Intel Xeon Phi), в презентации самостоятельной работы №1 указаны логин и пароль для доступ на кластер и выполнения самостоятельных работ, но войти по такой паре логин-пароль не получается. Как предполагается выполнение самосоятельных работ в этом курсе? |
Самостоятельная работа 4: Оптимизация расчетов на примере задачи вычисления справедливой цены опциона Европейского типа
Нулевая (базовая) версия
Одна из типичных ошибок, которые допускают многие программисты на языке C (не только начинающие), – смешивание типов float и double при работе с числами c плавающей запятой. Оставляя за рамками обсуждения вопросы точности получаемого результата, рассмотрим, как это влияет на скорость работы, если данные представлены типом float, а используемые математические функции вызываются в форме, работающей с типом double.
В представленном выше коде функции GetOptionPrice() мы использовали, например, функцию logf(). Если программист вызовет вместо нее функцию log(), результат будет следующий. Функция log() в языке C принимает аргумент типа double и возвращает число типа double. Вследствие этого, во-первых, элемент массива pT[i] будет преобразован из типа float в тип double и, во-вторых, далее все вычисления будут вестись с типом double (с преобразованием типов при необходимости) и лишь в самом конце при присваивании результата в элемент массива p1[0] будет выполнено обратное преобразование в тип float. Учитывая, что работа с числами типа float во многих случаях происходит быстрее, чем с типом double (в особенности заметна эта разница становится при использовании векторизации), подобные преобразования сказываются на скорости работы кода отрица тельно. Д авайте посмотрим, насколько велика будет эта разница в нашем случае.
Добавьте в файл main.cpp следующую функцию.
__declspec(noinline) void GetOptionPricesV0(
float *pT, float *pK, float *pS0, float *pC)
{
int i;
float d1, d2, p1, p2;
for (i = 0; i < N; i++)
{
d1 = (log(pS0[i] / pK[i]) + (r + sig * sig * 0.5) *
pT[i]) / (sig * sqrt(pT[i]));
d2 = (log(pS0[i] / pK[i]) + (r - sig * sig * 0.5) *
pT[i]) / (sig * sqrt(pT[i]));
p1 = cdfnormf(d1);
p2 = cdfnormf(d2);
pC[i] = pS0[i] * p1 - pK[i] *
exp((-1.0) * r * pT[i]) * p2;
}
}
Теперь модифицируем функцию main(). Добавим тип данных – указатель на функцию с прототипом, соответствующим функции GetOption-PricesV0(), объявим массив указателей GetOptionPrices, и по номеру модификации будем вызывать требуемую функцию.
#include "omp.h"
...
// Начало и конец счета
double start, finish;
// Время вычислений
double t;
typedef void (*tGetOptionPrices)(float *pT, float *pK,
float *pS0, float *pC);
tGetOptionPrices GetOptionPrices[9] =
{ GetOptionPricesV0 };
int main(int argc, char *argv[])
{
int i, version;
if (argc < 2)
{
printf("Usage: <executable> size version [#of_threads]\n");
return 1;
}
N = atoi(argv[1]);
version = atoi(argv[2]);
if (argc > 3)
numThreads = atoi(argv[3]);
pT = new float[4 * N];
pK = pT + N;
pS0 = pT + 2 * N;
pC = pT + 3 * N;
for (i = 0; i < N; i++)
{
pT[i] = T;
pS0[i] = S0;
pK[i] = K;
}
float res = GetOptionPrice();
printf("%.8f;\n", res);
omp_set_num_threads(numThreads);
start = omp_get_wtime();
GetOptionPrices[version](pT, pK, pS0, pC);
finish = omp_get_wtime();
t = finish - start;
printf("v%d: %.8f; %lf\n", version, pC[0], t);
delete [] pT;
return 0;
}
Отметим, что для удобства измерение времени выполнено с использованием функции OpenMP (учитывая, что в дальнейшем именно с помощью этой технологии мы будем разрабатывать параллельную версию).
Также отметим, что память под массивы, хранящие данные, мы выделяем одной операцией, настраивая далее указатели pT, pK, pS0 и pC.
Для корректной сборки программы в командную строку нужно добавить ключ -openmp:
icc -O2 -openmp ...
Соберите программу, проведите эксперименты, увеличивая число образцов вдвое от 60 000 000 до 240 000 000, и занесите их в таблицу.
На описанной ранее инфраструктуре авторы получили следующие времена.
| N | 60 000 000 | 120 000 000 | 180 000 000 | 240 000 000 |
| Версия V0 | 17,002 | 34,004 | 51,008 | 67,970 |
Версия 1. Исключение ненужных преобразований типов
Теперь приведем код предыдущей версии в порядок, вызывая правильные функций logf(),sqrtf() и expf() а также правильно указывая константы в коде (все знают, что число 1.0 без суффикса f будет рассматриваться как число типа double?).
__declspec(noinline) void GetOptionPricesV1(float *pT,
float *pK, float *pS0, float *pC)
{
int i;
float d1, d2, p1, p2;
for (i = 0; i < N; i++)
{
d1 = (logf(pS0[i] / pK[i]) + (r + sig * sig * 0.5f) *
pT[i]) / (sig * sqrtf(pT[i]));
d2 = (logf(pS0[i] / pK[i]) + (r - sig * sig * 0.5f) *
pT[i]) / (sig * sqrtf(pT[i]));
p1 = cdfnormf(d1);
p2 = cdfnormf(d2);
pC[i] = pS0[i] * p1 - pK[i] *
expf((-1.0f) * r * pT[i]) * p2;
}
}
Добавьте в массив указателей GetOptionPrices, новую функцию.
tGetOptionPrices GetOptionPrices[9] =
{ GetOptionPricesV0, GetOptionPricesV1 };
Соберите программу и проведите эксперименты.
На описанной ранее инфраструктуре авторы получили следующие времена.
| N | 60 000 000 | 120 000 000 | 180 000 000 | 240 000 000 |
| Версия V0 | 17,002 | 34,004 | 51,008 | 67,970 |
| Версия V1 | 16,776 | 33,549 | 50,337 | 66,989 |
Как видно из таблицы 9.3, время работы версии 1 в сравнении с базовой изменилось очень мало. Причина такого результата в функции cdfnormf(), точнее в ее крайне медленной работе, а также в том, что время работы cdfnormf() и cdfnorm() не отличается. На ее фоне выигрыш от исключения ненужных преобразований типов практически не заметен. В других случаях разница может быть очень значительной. Так, авторы данной работы сталкивались с ситуацией, когда разница времени достигала трех раз (!).