Как вы обрабатываете exp () с SSE2? - PullRequest
0 голосов
/ 20 декабря 2018

Я делаю код, который по существу использует преимущества SSE2 для оптимизации этого кода:

double *pA = a;
double *pB = b[voiceIndex];
double *pC = c[voiceIndex];

for (int sampleIndex = 0; sampleIndex < blockSize; sampleIndex++) {
    pC[sampleIndex] = exp((mMin + std::clamp(pA[sampleIndex] + pB[sampleIndex], 0.0, 1.0) * mRange) * ln2per12);
}

в этом:

double *pA = a;
double *pB = b[voiceIndex];
double *pC = c[voiceIndex];

// SSE2
__m128d bound_lower = _mm_set1_pd(0.0);
__m128d bound_upper = _mm_set1_pd(1.0);
__m128d rangeLn2per12 = _mm_set1_pd(mRange * ln2per12);
__m128d minLn2per12 = _mm_set1_pd(mMin * ln2per12);

__m128d loaded_a = _mm_load_pd(pA);
__m128d loaded_b = _mm_load_pd(pB);
__m128d result = _mm_add_pd(loaded_a, loaded_b);
result = _mm_max_pd(bound_lower, result);
result = _mm_min_pd(bound_upper, result);
result = _mm_mul_pd(rangeLn2per12, result);
result = _mm_add_pd(minLn2per12, result);

double *pCEnd = pC + roundintup8(blockSize);
for (; pC < pCEnd; pA += 8, pB += 8, pC += 8) {
    _mm_store_pd(pC, result);

    loaded_a = _mm_load_pd(pA + 2);
    loaded_b = _mm_load_pd(pB + 2);
    result = _mm_add_pd(loaded_a, loaded_b);
    result = _mm_max_pd(bound_lower, result);
    result = _mm_min_pd(bound_upper, result);
    result = _mm_mul_pd(rangeLn2per12, result);
    result = _mm_add_pd(minLn2per12, result);
    _mm_store_pd(pC + 2, result);

    loaded_a = _mm_load_pd(pA + 4);
    loaded_b = _mm_load_pd(pB + 4);
    result = _mm_add_pd(loaded_a, loaded_b);
    result = _mm_max_pd(bound_lower, result);
    result = _mm_min_pd(bound_upper, result);
    result = _mm_mul_pd(rangeLn2per12, result);
    result = _mm_add_pd(minLn2per12, result);
    _mm_store_pd(pC + 4, result);

    loaded_a = _mm_load_pd(pA + 6);
    loaded_b = _mm_load_pd(pB + 6);
    result = _mm_add_pd(loaded_a, loaded_b);
    result = _mm_max_pd(bound_lower, result);
    result = _mm_min_pd(bound_upper, result);
    result = _mm_mul_pd(rangeLn2per12, result);
    result = _mm_add_pd(minLn2per12, result);
    _mm_store_pd(pC + 6, result);

    loaded_a = _mm_load_pd(pA + 8);
    loaded_b = _mm_load_pd(pB + 8);
    result = _mm_add_pd(loaded_a, loaded_b);
    result = _mm_max_pd(bound_lower, result);
    result = _mm_min_pd(bound_upper, result);
    result = _mm_mul_pd(rangeLn2per12, result);
    result = _mm_add_pd(minLn2per12, result);
}

И я бы сказал, что он работает довольно хорошо.НО, не могу найти никакой функции exp для SSE2, чтобы завершить цепочку операций.

Чтение это , кажется, мне нужно вызвать стандартный exp() из библиотеки?

Правда?Разве это не наказывает?Есть ли другие способы?Другая встроенная функция?

Я нахожусь на MSVC, /arch:SSE2, /O2, производя 32-битный код.

Ответы [ 3 ]

0 голосов
/ 20 декабря 2018

Существует несколько библиотек, которые обеспечивают экспоненциальную векторизацию с большей или меньшей точностью.

  • SVML , предоставляемые с компилятором Intel (он также предоставляет встроенные функции, так что если выесть лицензия, вы можете использовать ее), имеет другой уровень точности (и скорости)
  • , о котором вы упомянули IPP , также от Intel, которые также предоставляют некоторые функции
  • MKL также предоставляет некоторый интерфейс для этого вычисления (для этого исправление ISA может быть выполнено с помощью макросов, например, если вам нужна воспроизводимость или точность)
  • fmath это еще один вариант, вы можете оторвать код из векторизованного exp, чтобы интегрировать его в ваш цикл.

По опыту все это быстрее и точнее, чем пользовательское приближение padde (даже не говоря онестабильное расширение Тейлора, которое бы ОЧЕНЬ быстро дало вам отрицательное число)из вашего цикла или вызова exp одним вызовом для вашего полного массива (поскольку библиотеки могут использовать AVX512 вместо только SSE2).

0 голосов
/ 20 декабря 2018

Реализация exp в SSE2 отсутствует, поэтому, если вы не хотите накатывать свои собственные, как предложено выше, одним из вариантов является использование инструкций AVX512 на некоторых аппаратных средствах, поддерживающих ERI (экспоненциальные и взаимные инструкции).См. https://en.wikipedia.org/wiki/AVX-512#New_instructions_in_AVX-512_exponential_and_reciprocal

Я думаю, что в настоящее время это ограничивает вас ксион фи (как указал Питер Кордес - я нашел одно утверждение о том, что это на Скайлэйке и Кэннонлейке, но не могу подтвердить это), и нестипомните также, что код не будет работать вообще (то есть будет аварийно завершаться) на других архитектурах.

0 голосов
/ 20 декабря 2018

Самый простой способ - использовать экспоненциальное приближение.Один возможный случай, основанный на этом пределе

enter image description here

Для n = 256 = 2^8:

__m128d fastExp1(__m128d x)
{
   __m128d ret = _mm_mul_pd(_mm_set1_pd(1.0 / 256), x);
   ret = _mm_add_pd(_mm_set1_pd(1.0), ret);
   ret = _mm_mul_pd(ret, ret);
   ret = _mm_mul_pd(ret, ret);
   ret = _mm_mul_pd(ret, ret);
   ret = _mm_mul_pd(ret, ret);
   ret = _mm_mul_pd(ret, ret);
   ret = _mm_mul_pd(ret, ret);
   ret = _mm_mul_pd(ret, ret);
   ret = _mm_mul_pd(ret, ret);
   return ret;
}

Другая идея - разложение по полиному.В частности, разложение в ряд Тейлора:

enter image description here

__m128d fastExp2(__m128d x)
{
   const __m128d a0 = _mm_set1_pd(1.0);
   const __m128d a1 = _mm_set1_pd(1.0);
   const __m128d a2 = _mm_set1_pd(1.0 / 2);
   const __m128d a3 = _mm_set1_pd(1.0 / 2 / 3);
   const __m128d a4 = _mm_set1_pd(1.0 / 2 / 3 / 4);
   const __m128d a5 = _mm_set1_pd(1.0 / 2 / 3 / 4 / 5);
   const __m128d a6 = _mm_set1_pd(1.0 / 2 / 3 / 4 / 5 / 6);
   const __m128d a7 = _mm_set1_pd(1.0 / 2 / 3 / 4 / 5 / 6 / 7);

   __m128d ret = _mm_fmadd_pd(a7, x, a6);
   ret = _mm_fmadd_pd(ret, x, a5); 
   // If fma extention is not present use
   // ret = _mm_add_pd(_mm_mul_pd(ret, x), a5);
   ret = _mm_fmadd_pd(ret, x, a4);
   ret = _mm_fmadd_pd(ret, x, a3);
   ret = _mm_fmadd_pd(ret, x, a2);
   ret = _mm_fmadd_pd(ret, x, a1);
   ret = _mm_fmadd_pd(ret, x, a0);
   return ret;
}

Обратите внимание, что при одинаковом количестве членов разложения вы можете получить лучшее приближение, если приблизитефункция для определенного диапазона x, используя, например, метод наименьших квадратов.

Все эти методы работают в очень ограниченном диапазоне x , но с непрерывными производными, которые могут быть важны в некоторых случаях,

Существует хитрость для аппроксимации показателя в очень широком диапазоне , но с заметными кусочно-линейными областями.Он основан на реинтерпретации целых чисел как чисел с плавающей точкой.Для более точного описания я рекомендую следующие ссылки:

Кусочно-линейное приближение к экспоненте и логарифму

Быстрое, компактное приближение экспоненциальной функции

Возможная реализация этого подхода:

__m128d fastExp3(__m128d x)
{
   const __m128d a = _mm_set1_pd(1.0 / M_LN2);
   const __m128d b = _mm_set1_pd(3 * 1024.0 - 1.05);
   __m128d t = _mm_fmadd_pd(x, a, b);
   return _mm_castsi128_pd(_mm_slli_epi64(_mm_castpd_si128(t), 11));
}

Несмотря на простоту и широкий диапазон x для этого метода, будьте осторожны при использовании в математике.В небольших областях это дает кусочную аппроксимацию, которая может нарушить чувствительные алгоритмы, особенно те, которые используют дифференцирование.

Чтобы сравнить точность различных методов, посмотрите на графику.Первый график сделан для диапазона x = [0..1).Как видите, наилучшее приближение в этом случае дается методом fastExp2(x), чуть хуже, но приемлемым является fastExp1(x).Наихудшее приближение дает fastExp3(x) - кусочная структура заметна, разрывы первой производной есть.

enter image description here В диапазоне x = [0..10) *Метод 1054 * обеспечивает наилучшую аппроксимацию, чуть хуже - аппроксимацию, заданную fastExp1(x) - при том же количестве вычислений он обеспечивает больший порядок, чем fastExp2(x).enter image description here

Следующим шагом является повышение точности алгоритма fastExp3(x).Самый простой способ значительно повысить точность - использовать равенство exp(x) = exp(x/2)/exp(-x/2). Хотя это увеличивает объем вычислений, оно значительно уменьшает погрешность из-за взаимной компенсации ошибок при делении.

__m128d fastExp5(__m128d x)
{
   const __m128d ap = _mm_set1_pd(0.5 / M_LN2);
   const __m128d an = _mm_set1_pd(-0.5 / M_LN2);
   const __m128d b = _mm_set1_pd(3 * 1024.0 - 1.05);
   __m128d tp = _mm_fmadd_pd(x, ap, b);
   __m128d tn = _mm_fmadd_pd(x, an, b);
   tp = _mm_castsi128_pd(_mm_slli_epi64(_mm_castpd_si128(tp), 11));
   tn = _mm_castsi128_pd(_mm_slli_epi64(_mm_castpd_si128(tn), 11));
   return _mm_div_pd(tp, tn);
}

enter image description here

Еще большей точности можно достичь, комбинируя методы из алгоритмов fastExp1(x) или fastExp2(x) и fastExp3(x) с использованием равенства exp(x+dx) = exp(x) *exp(dx).Как показано выше, первый множитель может быть вычислен аналогично подходу fastExp3(x), для второго умножителя может использоваться метод fastExp1(x) или fastExp2(x).Поиск оптимального решения в этом случае является довольно сложной задачей, и я бы рекомендовал взглянуть на реализацию в библиотеках, предложенных в ответах.

...