Нижегородский государственный университет им. Н.И.Лобачевского
Опубликован: 30.05.2014 | Доступ: свободный | Студентов: 302 / 34 | Длительность: 11:26:00

Самостоятельная работа 4: Оптимизация расчетов на примере задачи вычисления справедливой цены опциона Европейского типа

Версия 4. Векторизация: использование директивы simd

Второй способ векторизации связан с использованием перед циклом директивы #pragma ivdep (ignore vector dependencies). Она подсказывает компилятору, что с нашей точки зрения массивы в цикле не пересекаются. Заметим, что в случае, когда мы ошиблись, компилятор иногда может установить наличие зависимости и проигнорировать нашу подсказку, но чаще всего мы получим некорректно работающий код. Данной директивой надо пользоваться с осторожностью. Заметим, что эта директива иногда используется в паре с #pragma vector always – подсказкой компилятору о том, что с нашей точки зрения векторизация, если она возможна, будет эффективна. Как мы упоминали ранее в лекции и практике по векторизации, иногда векторизация не приведет к ускорению, более того, может возникнуть замедление. Как правило, это происходит в тех ситуациях, когда данные лежат в памяти не в том порядке, в котором их нужно запаковывать в векторные регистры. В таких случаях накладные расходы на подготовку данных и запись результа тов могут превысить выигрыш от векторного выполнения операций. Однако компилятор не всегда прав. Иногда он считает векторизацию неэффективной, но мы хотим попробовать. Для этого и нужна #pragma vector always.

В настоящий момент основным становится третий способ подсказки компилятору – использование новой директивы #pragma simd. Возможности компилятора по векторизации цикла при использовании данной директивы значительно выше, кроме того, существует целый ряд настроек, которых не было ранее. Заметим, что ответственность за корректность результата в этом случае полностью ложится на программиста. Использовать данную директиву нужно с осторожностью.

__declspec(noinline) void GetOptionPricesV4(float *pT,
  float *pK, float *pS0, float *pC)
{
  int i;
  float d1, d2, erf1, erf2;

#pragma simd
  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]));
    erf1 = 0.5f + 0.5f * erff(d1 / sqrtf(2.0f));
    erf2 = 0.5f + 0.5f * erff(d2 / sqrtf(2.0f));
    pC[i] = pS0[i] * erf1 - pK[i] * expf((-1.0f) * r * 
            pT[i]) * erf2;
  }
}

Добавьте в массив указателей GetOptionPrices, новую функцию.

tGetOptionPrices GetOptionPrices[9] = 
{ GetOptionPricesV0, GetOptionPricesV1, GetOptionPricesV2,
  GetOptionPricesV3, GetOptionPricesV4 };

Соберите программу и проведите эксперименты.

На описанной ранее инфраструктуре авторы получили следующие времена.

Таблица 9.6. Время работы версий с 0 до 4 (в секундах)
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
Версия V2 2,871 5,727 8,649 11,230
Версия V3 0,522 1,049 1,583 2,091
Версия V4 0,521 1,036 1,566 2,067

Как видно из таблицы 9.6, время работы версии 4 почти не отличается от времени версии 3. Впрочем, этого и следовало ожидать.

Версия 5. Вынос инвариантов из цикла

Попытаться еще немного ускорить работу функции GetOption-PricesV4() можно, сэкономив не тех вычислениях, которые можно вынести за цикл. В данном случае это расчет 1.0f / sqrtf(2.0f) .

Вычислим это выражение отдельно и внесем в код константу вида

const float invsqrt2 = 0.707106781f;

Теперь используем ее в теле функции, заменив попутно деление на умножение.

__declspec(noinline) void GetOptionPricesV5(float *pT,
  float *pK, float *pS0, float *pC)
{
  int i;
  float d1, d2, erf1, erf2;

#pragma simd
  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]));
    erf1 = 0.5f + 0.5f * erff(d1 * invsqrt2);
    erf2 = 0.5f + 0.5f * erff(d2 * invsqrt2);
    pC[i] = pS0[i] * erf1 - pK[i] * expf((-1.0f) * r * 
             pT[i]) * erf2;
  }
}

Добавьте в массив указателей GetOptionPrices , новую функцию.

tGetOptionPrices GetOptionPrices[9] = 
{ GetOptionPricesV0, GetOptionPricesV1, GetOptionPricesV2,
  GetOptionPricesV3, GetOptionPricesV4, GetOptionPricesV5
};

Соберите программу и проведите эксперименты.

На описанной ранее инфраструктуре авторы получили следующие времена.

Таблица 9.7. Время работы версий с 0 до 5 (в секундах)
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
Версия V2 2,871 5,727 8,649 11,230
Версия V3 0,522 1,049 1,583 2,091
Версия V4 0,521 1,036 1,566 2,067
Версия V5 0,527 1,047 1,580 2,085

Как видно из таблицы 9.7, время работы версии 5 практически совпадает с временами версий 3 и 4. В данном случае, видимо, компилятор проделал необходимые преобразования кода самостоятельно. Убедиться в этом можно, изучив ассемблерный листинг программы после сборки (укажите в командной строке ключ -Fa ).

Заметим, что хотя в данном случае мы ничего не выиграли, вынос инвариантов – полезная техника оптимизации, которая, как правило, делает код более понятным и логичным, а также нередко приводит к выигрышу производительности (компилятор далеко не всегда может осуществить подобные преобразования сам).

Svetlana Svetlana
Svetlana Svetlana

Здравствуйие! Я хочу пройти курс Введение в принципы функционирования и применения современных мультиядерных архитектур (на примере Intel Xeon Phi), в презентации самостоятельной работы №1 указаны логин и пароль для доступ на кластер и выполнения самостоятельных работ, но войти по такой паре логин-пароль не получается. Как предполагается выполнение самосоятельных работ в этом курсе?