Здравствуйие! Я хочу пройти курс Введение в принципы функционирования и применения современных мультиядерных архитектур (на примере Intel Xeon Phi), в презентации самостоятельной работы №1 указаны логин и пароль для доступ на кластер и выполнения самостоятельных работ, но войти по такой паре логин-пароль не получается. Как предполагается выполнение самосоятельных работ в этом курсе? |
Самостоятельная работа 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 };
Соберите программу и проведите эксперименты.
На описанной ранее инфраструктуре авторы получили следующие времена.
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 };
Соберите программу и проведите эксперименты.
На описанной ранее инфраструктуре авторы получили следующие времена.
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 ).
Заметим, что хотя в данном случае мы ничего не выиграли, вынос инвариантов – полезная техника оптимизации, которая, как правило, делает код более понятным и логичным, а также нередко приводит к выигрышу производительности (компилятор далеко не всегда может осуществить подобные преобразования сам).