Как преобразовать скалярный код двойной версии VDT Pade Exp fast_ex() приблизительно в SSE2?

Вот код, который я пытаюсь преобразовать: double версия VDT's Pade Exp fast_ex() приблизительно (вот старый репозиторий ресурс):

inline double fast_exp(double initial_x){
    double x = initial_x;
    double px=details::fpfloor(details::LOG2E * x +0.5);

    const int32_t n = int32_t(px);

    x -= px * 6.93145751953125E-1;
    x -= px * 1.42860682030941723212E-6;

    const double xx = x * x;

    // px = x * P(x**2).
    px = details::PX1exp;
    px *= xx;
    px += details::PX2exp;
    px *= xx;
    px += details::PX3exp;
    px *= x;

    // Evaluate Q(x**2).
    double qx = details::QX1exp;
    qx *= xx;
    qx += details::QX2exp;
    qx *= xx;
    qx += details::QX3exp;
    qx *= xx;
    qx += details::QX4exp;

    // e**x = 1 + 2x P(x**2)/( Q(x**2) - P(x**2) )
    x = px / (qx - px);
    x = 1.0 + 2.0 * x;

    // Build 2^n in double.
    x *= details::uint642dp(( ((uint64_t)n) +1023)<<52);

    if (initial_x > details::EXP_LIMIT)
      x = std::numeric_limits<double>::infinity();
    if (initial_x < -details::EXP_LIMIT)
      x = 0.;

    return x; 
}

Я получил это:

__m128d PExpSSE_dbl(__m128d x) {
    __m128d initial_x = x;

    __m128d half = _mm_set1_pd(0.5);
    __m128d one = _mm_set1_pd(1.0);
    __m128d log2e = _mm_set1_pd(1.4426950408889634073599);
    __m128d p1 = _mm_set1_pd(1.26177193074810590878E-4);
    __m128d p2 = _mm_set1_pd(3.02994407707441961300E-2);
    __m128d p3 = _mm_set1_pd(9.99999999999999999910E-1);
    __m128d q1 = _mm_set1_pd(3.00198505138664455042E-6);
    __m128d q2 = _mm_set1_pd(2.52448340349684104192E-3);
    __m128d q3 = _mm_set1_pd(2.27265548208155028766E-1);
    __m128d q4 = _mm_set1_pd(2.00000000000000000009E0);

    __m128d px = _mm_add_pd(_mm_mul_pd(log2e, x), half);
    __m128d t = _mm_cvtepi64_pd(_mm_cvttpd_epi64(px));  
    px = _mm_sub_pd(t, _mm_and_pd(_mm_cmplt_pd(px, t), one));

    __m128i n = _mm_cvtpd_epi64(px);

    x = _mm_sub_pd(x, _mm_mul_pd(px, _mm_set1_pd(6.93145751953125E-1)));
    x = _mm_sub_pd(x, _mm_mul_pd(px, _mm_set1_pd(1.42860682030941723212E-6)));
    __m128d xx = _mm_mul_pd(x, x);

    px = _mm_mul_pd(xx, p1);
    px = _mm_add_pd(px, p2);
    px = _mm_mul_pd(px, xx);
    px = _mm_add_pd(px, p3);
    px = _mm_mul_pd(px, x);

    __m128d qx = _mm_mul_pd(xx, q1);
    qx = _mm_add_pd(qx, q2);
    qx = _mm_mul_pd(xx, qx);
    qx = _mm_add_pd(qx, q3);
    qx = _mm_mul_pd(xx, qx);
    qx = _mm_add_pd(qx, q4);

    x = _mm_div_pd(px, _mm_sub_pd(qx, px));
    x = _mm_add_pd(one, _mm_mul_pd(_mm_set1_pd(2.0), x));

    n = _mm_add_epi64(n, _mm_set1_epi64x(1023));
    n = _mm_slli_epi64(n, 52);

    // return?
}

Но я не могу закончить последние строки, то есть этот код:

    if (initial_x > details::EXP_LIMIT)
      x = std::numeric_limits<double>::infinity();
    if (initial_x < -details::EXP_LIMIT)
      x = 0.;

    return x; 

Как бы вы конвертировали в SSE2?

Тогда, конечно, мне нужно проверить все, так как я не совсем уверен, что преобразовал его правильно.

EDIT: я нашел SSE-преобразование float exp, т.е. из этого:

/* multiply by power of 2 */
z *= details::uint322sp((n + 0x7f) << 23);

if (initial_x > details::MAXLOGF) z = std::numeric_limits<float>::infinity();
if (initial_x < details::MINLOGF) z = 0.f;

return z;

к этому:

n = _mm_add_epi32(n, _mm_set1_epi32(0x7f));
n = _mm_slli_epi32(n, 23);

return _mm_mul_ps(z, _mm_castsi128_ps(n));

person markzzz    schedule 25.01.2019    source источник
comment
ссылка указывает на документацию умеренно старой версии, код поддерживается в проекте vdt: здесь   -  person pseyfert    schedule 25.01.2019
comment
@pseyfert: спасибо! Этот конкретный код кажется таким же, хотя   -  person markzzz    schedule 25.01.2019
comment
Обновил мой ответ; первая версия была кратким постом, и я не стал вдаваться в подробности.   -  person Peter Cordes    schedule 26.01.2019
comment
32-битный сдвиг влево на 23, очевидно, вставляет биты в поле экспоненты одинарной точности float, а не double. то есть умножение числа с плавающей запятой на 1<<n. Он не реализует проверку диапазона, только оператор z *= . У вас уже есть часть n = _mm_add_epi64(n, _mm_set1_epi64x(1023)); / n = _mm_slli_epi64(n, 52);, реализованная в вашем коде. для double. Я не заметил, что вы пропустили часть _mm_mul_pd(result, _mm_castsi128_pd(n)). Поэтому вы цитировали этот код с одинарной точностью?   -  person Peter Cordes    schedule 28.01.2019
comment
@PeterCordes да, в основном, я просто пишу, как кто-то переводит версию с плавающей запятой в simd. Это очень похоже на код между float и double, не так ли?   -  person markzzz    schedule 28.01.2019
comment
Да конечно похоже. IEEE binary32 и binary64 идентичны, за исключением ширины полей. en.wikipedia.org/wiki/Single-precision_format_floating-point против en.wikipedia.org/wiki/Double-precision_format_floating-point. Я не уверен, есть ли новая часть вопроса или почему вы цитируете это. Это только для строки _mm_mul_ps / _pd, которую вы пропустили, со встроенным каламбуром _mm_cast? Или вы говорите, что чья-то версия с плавающей запятой не учитывала диапазон? Это вполне возможно, если они решили, что им не нужны накладные расходы на проверку диапазона.   -  person Peter Cordes    schedule 28.01.2019
comment
@PeterCordes: о нет :) Я имею в виду, что версия с плавающей запятой, кажется, переводит всю последнюю часть кода в 1 строку кода, хотя кажется, что вы предлагаете много строк. Или я ошибаюсь?   -  person markzzz    schedule 28.01.2019
comment
@PeterCordes: да, кажется, он игнорирует вне диапазона :) Я бы сказал, что тоже могу игнорировать...   -  person markzzz    schedule 28.01.2019
comment
_mm_mul_ps(z, _mm_castsi128_ps(n)) просто реализует *= часть z *= details::uint322sp((n + 0x7f) << 23);. Я думал, что это очевидно. Конечно, для правильной реализации проверки диапазона потребуется несколько встроенных функций, особенно если у вас нет SSE4 для blendv. Однако было бы проще и дешевле реализовать это, заставив результат быть NaN. Но все же не так дешево, как вообще ничего.   -  person Peter Cordes    schedule 28.01.2019
comment
@PeterCordes в моем случае диапазон исходит из таблицы поиска, где я уже терплю, что он не будет за пределами диапазона (надеюсь: P).   -  person markzzz    schedule 28.01.2019
comment
Я пытался понять, нужно ли вам двойное двойное округление до ближайшего целого числа, или вам это нужно только в процессе преобразования в целое число (например, Как to floor/int в double, используя только SSE2?). Я заметил, что результат x = _mm_sub_pd(t, _mm_and_pd(_mm_cmplt_pd(px, t), one)); никогда не используется; вы позже делаете x = _mmstuff(px, ... px). Так что это странно.   -  person Peter Cordes    schedule 29.01.2019
comment
@PeterCordes: это цена моей неопытности в переводе скалярного кода в векторизованный, извините. Произошла ошибка: это x = _mm_sub_pd(x, _mm_mul_pd(px, _mm_set1_pd(6.93145751953125E-1))); ... Я исправил код.   -  person markzzz    schedule 29.01.2019


Ответы (1)


Да, деление двух многочленов часто может дать вам лучший компромисс между скоростью и точностью, чем один огромный многочлен. Пока есть достаточно работы, чтобы скрыть пропускную способность divpd. (Последние процессоры x86 имеют довольно приличную пропускную способность деления FP. Все еще плохое по сравнению с умножением, но это всего 1 моп, поэтому он не останавливает конвейер, если вы используете его достаточно редко, т.е. смешиваете с большим количеством умножений. В том числе в окружающем коде который использует exp)


Однако _mm_cvtepi64_pd(_mm_cvttpd_epi64(px)); не работает с SSE2. Встроенные функции упакованного преобразования в/из 64-битных целых чисел требуют AVX512DQ.

Для пакетного округления до ближайшего целого числа в идеале следует использовать SSE4.1 _mm_round_pd(x, _MM_FROUND_TO_NEAREST_INT |_MM_FROUND_NO_EXC) (или усечение в сторону нуля, или нижний или нижний предел в сторону -+Inf).

Но нам это на самом деле не нужно.

Скалярный код заканчивается тем, что int n и double px представляют одно и то же числовое значение. Он использует bad/buggy floor(val+0.5) вместо rint(val) или nearbyint(val) для округлить до ближайшего, а затем преобразовать это уже целое число double в int (с семантикой усечения C++, но это не имеет значения, поскольку значение double уже является точным целым числом).

Со встроенными функциями SIMD кажется, что проще всего просто преобразовать в 32-битное целое число и обратно.

__m128i n  = _mm_cvtpd_epi32( _mm_mul_pd(log2e, x) );   // round to nearest
__m128d px = _mm_cvtepi32_pd( n );

Округление до int с желаемым режимом, а затем обратное преобразование в double эквивалентно округлению double->double с последующим захватом версии int, как это делает скалярная версия. (Потому что вас не волнует, что произойдет, если двойные числа слишком велики, чтобы поместиться в целое число.)

Инструкции cvtsd2si и si2sd выполняются по 2 микрооперации каждая и перемешивают 32-битные целые числа, чтобы они были упакованы в младшие 64 бита вектора. Таким образом, чтобы настроить 64-битные целочисленные сдвиги, чтобы снова вставить биты в double, вам нужно перетасовать. Верхние 64 бита n будут нулями, поэтому мы можем использовать это для создания 64-битного целого числа n, выровненного с удвоениями:

n = _mm_shuffle_epi32(n, _MM_SHUFFLE(3,1,2,0));   // 64-bit integers

Но только с SSE2 есть обходные пути. Преобразование в 32-битное целое число и обратно — один из вариантов: вас не волнуют слишком маленькие или слишком большие входные данные. Но пакетное преобразование между double и int стоит как минимум 2 мопов на процессорах Intel в каждом направлении, поэтому всего 4. Но только 2 из этих мопов нуждаются в модулях FMA, и ваш код, вероятно, не будет узким местом на порту 5 со всеми этими умножает и складывает.

Или добавьте очень большое число и вычтите его снова: достаточно большое, чтобы каждое double отличалось на 1 целое число, поэтому обычное округление FP делает то, что вы хотите. (Это работает для входных данных, которые не помещаются в 32 бита, но не double > 2 ^ 52. Так что в любом случае это будет работать.) Также см. Как эффективно выполнять преобразования double/int64 с помощью SSE/AVX?, который использует этот прием. Однако я не смог найти пример на SO.


Связанный:

Тогда, конечно, мне нужно проверить все, так как я не совсем уверен, что преобразовал его правильно.

перебор всех 2^64 битовых шаблонов double нецелесообразен, в отличие от float, где их всего 4 миллиарда, но, возможно, перебор всех double, у которых младшие 32 бита мантиссы равны нулю, было бы хорошим началом. то есть проверить в цикле с

bitpatterns = _mm_add_epi64(bitpatterns, _mm_set1_epi64x( 1ULL << 32 ));
doubles = _mm_castsi128_pd(bitpatterns);

https://randomascii.wordpress.com/2014/01/27/theres-only-four-billion-floatsso-test-them-all/


Для этих последних нескольких строк исправление ввода для ввода вне допустимого диапазона:

Версия float, которую вы цитируете, полностью исключает проверку диапазона. Это, очевидно, самый быстрый способ, если ваши входные данные всегда будут в допустимом диапазоне или если вас не волнует, что произойдет с входными данными, выходящими за пределы допустимого диапазона.

Альтернативной более дешевой проверкой диапазона (возможно, только для отладки) было бы преобразование значений вне диапазона в NaN путем ИЛИ результата упакованного сравнения в результат. (Битовый шаблон «все единицы» представляет собой NaN.)

__m128d out_of_bounds = _mm_cmplt_pd( limit, abs(initial_x) );  // abs = mask off the sign bit
result = _mm_or_pd(result, out_of_bounds);

Как правило, вы можете векторизовать простую настройку условия значения, используя сравнение без ветвления + смешивание. Вместо if(x) y=0; у вас есть SIMD-эквивалент y = (condition) ? 0 : y; для каждого элемента. Сравнение SIMD создает маску из нулевых/всех элементов, чтобы вы могли использовать ее для смешивания.

например в этом случае cmppd ввод и blendvpd вывод, если у вас SSE4.1. Или просто с SSE2, и/и не/или блендить. См. встроенные параметры SSE для сравнения (_mm_cmpeq_ps) и операции присваивания для _ps версии обоих, _pd идентично.

На ассемблере это будет выглядеть так:

; result in xmm0  (in need of fixups for out of range inputs)
; initial_x in xmm2
; constants:
;     xmm5 = limit
;     xmm6 = +Inf
cmpltpd  xmm2, xmm5    ; xmm2 = input_x < limit ? 0xffff... : 0
andpd    xmm0, xmm2    ; result = result or 0
andnpd   xmm2, xmm6    ; xmm2 =  0 or +Inf   (In that order because we used ANDN)
orpd     xmm0, xmm2    ; result |= 0 or +Inf
; xmm0 = (input < limit) ? result : +Inf

(В более ранней версии ответа я думал, что, возможно, сохраняю movaps для копирования регистра, но это просто смесь болотного стандарта. Она уничтожает initial_x, поэтому компилятору необходимо скопировать этот регистр в какой-то момент при вычислении result , хотя.)


Оптимизация для этого особого условия

Или в этом случае 0.0 представлен битовым шаблоном со всеми нулями, поэтому сделайте сравнение, которое даст true, если оно находится в диапазоне, и И результат с этим. (Чтобы оставить его без изменений или заставить его +0.0). Это лучше, чем _mm_blendv_pd, который стоит 2 мкп на большинстве процессоров Intel (а 128-битная версия AVX всегда стоит 2 мкп на Intel). И не хуже на AMD или Skylake.

+-Inf представлен битовым шаблоном мантиссы=0, показатель степени=все единицы. (Любое другое значение в мантиссе представляет +-NaN.) Поскольку слишком большие входные данные, по-видимому, по-прежнему будут оставлять ненулевые мантиссы, мы не можем просто И сравнить результат и ИЛИ с окончательным результатом. Я думаю, нам нужно сделать обычную смесь или что-то более дорогое (3 мкп и векторная константа).

Это добавляет 2 цикла задержки к конечному результату; и ANDNPD, и ORPD находятся на критическом пути. CMPPD и ANDPD - нет; они могут работать параллельно с тем, что вы делаете для вычисления результата.

Надеюсь, ваш компилятор на самом деле будет использовать ANDPS и т. д., а не PD, для всего, кроме CMP, потому что он на 1 байт короче, но идентичен, потому что они оба просто побитовые операции. Я написал ANDPD только для того, чтобы не объяснять это в комментариях.


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

Или, поскольку ваши верхняя и нижняя границы имеют одинаковую величину, может быть, вы можете сравнить абсолютное значение? (замаскируйте бит знака initial_x и выполните _mm_cmplt_pd(abs_initial_x, _mm_set1_pd(details::EXP_LIMIT))). Но тогда вам придется разобраться, стоит ли обнулить или установить +Inf.

Если бы у вас был SSE4.1 для _mm_blendv_pd, вы могли бы использовать сам initial_x в качестве элемента управления наложением для исправления, которое может потребоваться применить, потому что blendv заботится только о бите знака элемента управления наложением (в отличие от версии AND/ANDN/OR, где все биты должны совпадать.)

__m128d  fixup = _mm_blendv_pd( _mm_setzero_pd(), _mm_set1_pd(INFINITY), initial_x );  // fixup = (initial_x signbit) ? 0 : +Inf
 // see below for generating fixup with an SSE2 integer arithmetic-shift

const  signbit_mask = _mm_castsi128_pd(_mm_set1_epi64x(0x7fffffffffffffff));  // ~ set1(-0.0)
__m128d  abs_init_x = _mm_and_pd( initial_x, signbit_mask );

__m128d out_of_range = _mm_cmpgt_pd(abs_init_x, details::EXP_LIMIT);

// Conditionally apply the fixup to result
result = _mm_blendv_pd(result, fixup, out_of_range);

Возможно, используйте cmplt вместо cmpgt и измените порядок, если вам важно, что произойдет, если initial_x будет NaN. Выбор сравнения таким образом, что false применяет исправление вместо true, будет означать, что неупорядоченное сравнение приводит либо к 0, либо к +Inf для ввода -NaN или +NaN. Это по-прежнему не распространяет NaN. Вы можете _mm_cmpunord_pd(initial_x, initial_x) и ИЛИ это в fixup, если хотите, чтобы это произошло.

Особенно на Skylake и AMD Bulldozer/Ryzen, где SSE2 blendvpd составляет всего 1 микрооперацию, это должно быть довольно неплохо. (Кодировка VEX, vblendvpd, составляет 2 моп, имеет 3 входа и отдельный выход.)

Возможно, вы все еще сможете использовать часть этой идеи только с SSE2, возможно, создав fixup, выполнив сравнение с нулем, а затем _mm_and_pd или _mm_andnot_pd с результатом сравнения и + Infinity.


Использование целочисленного арифметического сдвига для трансляции бита знака в каждую позицию в double неэффективно: psraq не существует, только psraw/d. Только логические сдвиги имеют 64-битный размер элемента.

Но вы можете создать fixup всего одним целочисленным сдвигом и маской, а также побитовым инвертированием

__m128i  ix = _mm_castsi128_pd(initial_x);
__m128i ifixup = _mm_srai_epi32(ix, 11);               // all 11 bits of exponent field = sign bit
ifixup = _mm_and_si128(ifixup, _mm_set1_epi64x(0x7FF0000000000000ULL) );  // clear other bits
// ix = the bit pattern for 0 (non-negative x) or +Inf (negative x)  

__m128d fixup = _mm_xor_si128(ifixup, _mm_set1_epi32(-1));  // bitwise invert

Затем смешайте fixup с result для ввода вне диапазона, как обычно.


Недорогая проверка abs(initial_x) > details::EXP_LIMIT

Если бы алгоритм exp уже возводил в квадрат initial_x, вы могли бы сравнить с EXP_LIMIT в квадрате. Но это не так, xx = x*x происходит только после некоторых вычислений для создания x.


Если у вас есть AVX512F/VL, здесь может пригодиться VFIXUPIMMPD. Он предназначен для функций, в которых выходные данные особого случая берутся из «особых» входных данных, таких как NaN и +-Inf, отрицательных, положительных или нулевых, с сохранением сравнения для этих случаев. (например, после обратной величины Ньютона-Рафсона (x) для x = 0.)

Но оба ваших особых случая нуждаются в сравнении. Или они?

Если вы возведете входные данные в квадрат и вычтете, потребуется всего один FMA, чтобы выполнить initial_x * initial_x - details::EXP_LIMIT * details::EXP_LIMIT, чтобы получить отрицательный результат для abs(initial_x) < details::EXP_LIMIT и неотрицательный в противном случае.

Агнер Фог сообщает, что vfixupimmpd находится всего в 1 моп на Skylake-X.

person Peter Cordes    schedule 26.01.2019
comment
Я чувствую, что должно быть что-то, что вы можете сделать с целочисленным сравнением с битовым шаблоном, который у вас уже есть в какой-то момент, чтобы проверить величину initial_x. (Целое значение битового шаблона монотонно увеличивается по мере увеличения double, за исключением знака/величины по сравнению с дополнением до двух), поэтому nextafter можно выполнить с целым приращением.) Вот почему поле экспоненты смещено. - person Peter Cordes; 27.01.2019
comment
Например, сравнить сдвинутый вправо ifixup со смещенным вправо details::EXP_LIMIT. Но с srai/and вы не удаляете бит знака, пока вы также не выбросите все биты, кроме копий бита знака в поле экспоненты. - person Peter Cordes; 27.01.2019
comment
пожалуйста, проверьте последнее редактирование, которое я сделал для этого вопроса: может ли это помочь? Не совсем уверен, что вы предлагаете мне это сделать, но прежде чем узнать потрясающий ответ, который вы мне даете, я был бы уверен, что смогу быстро сделать что-то подобное :) - person markzzz; 28.01.2019
comment
ДА! Вы были правы: _mm_cvtepi64_pd(_mm_cvttpd_epi64(px)); авария. Как я могу это исправить? Должен ли я открыть новый вопрос? - person markzzz; 28.01.2019
comment
@markzzz: Да, вы используете MSVC, который позволяет вам использовать встроенные функции для наборов инструкций, которые вы не включили. (GCC не позволил бы ему скомпилироваться без -march=skylake-avx512). В любом случае, да, откройте новый вопрос. Я искал, но не нашел точного дубликата того, как усечь __m128d до следующего целого числа наименьшей величины без SSE4.1 для roundpd. В вашем случае имеют значение только входные данные небольшой величины, поэтому добавление огромного числа и его повторное вычитание будет работать, как говорит мой ответ, поэтому вы, вероятно, можете опубликовать ответ на новый вопрос, если сможете получить правильную константу. - person Peter Cordes; 28.01.2019
comment
учитывая, что исходная двойная функция будет работать в диапазоне +708/-708, вероятно, _mm_cvtpd_epi32 будет достаточно без какой-либо потери точности... - person markzzz; 28.01.2019
comment
Я имел в виду, что и в исходном коде github.com/dpiparo/vdt /blob/master/include/exp.h#L75 приводил double к int32, поэтому, вероятно, достаточно _mm_cvtpd_epi32 (поскольку _mm_cvtpd_epi64 на SSE2 его не существует). Так же и с _mm_cvtepi32_pd(_mm_cvttpd_epi32(px)) должно хватить, да? - person markzzz; 29.01.2019
comment
@markzzz: да, но вам, вероятно, следует использовать одно и то же преобразование для обоих случаев, а не усечение для одного и ближайшее для другого. Разве алгоритм не пытается разделить целые и дробные части? В скалярной версии выполняется приведение к типу int (т. е. с усечением) значения, которое уже является целым числом из floor, поэтому не имеет значения, какой режим округления используется. Поэтому вам, вероятно, следует сделать n = _mm_cvtpd_epi32(something); (округлить до ближайшего) / px = _mm_cvtepi32_pd(n) (преобразовать обратно). Таким образом, всего 2 преобразования. - person Peter Cordes; 29.01.2019
comment
Было бы неплохо избежать дополнительной перетасовки, которую делает преобразование в 32-битный int, но я не думаю, что мы можем это сделать. Если только, возможно, приемы Mysticial IEEE для double-›int не являются более эффективными в целом. Но вам все еще нужна дробная часть как двойная, поэтому вам нужно либо преобразовать обратно, либо расширить хитрые вещи. - person Peter Cordes; 29.01.2019
comment
Не уверен, что вы имеете в виду. Я думаю, что он использует подход floor(d + 0.5) вместо round(), потому что последний зависит от режима округления системы. Вместо этого с floor(d + 0.5) вы гарантируете, что округление будет округлять to-nearest каждый раз. Таким образом, нужен _mm_cvtepi32_pd(_mm_cvttpd_epi32(px)) (и не только _mm_cvtepi32_pd). Если вы делаете только _mm_cvtepi32_pd, вы не уверены, что округление равно to-nearest (потому что, как сказано, зависит от fegetround()). Я ошибся? - person markzzz; 29.01.2019
comment
@markzzz: вы можете предположить, что режим округления является ближайшим, если вы не изменили его. Нормальный код FP всегда предполагает режим округления по умолчанию. Оптимизация компилятора и оценка во время компиляции / распространение констант предполагают это, если вы не используете #pragma STDC FENV_ACCESS ON и/или не компилируете со специальными параметрами. Другими словами, никто не ожидает, что вы можете установить режим округления, а затем вызвать другие функции и ожидать, что они сработают. Не менее важно и то, что усечение в сторону 0 не то же самое, что пол в сторону -бесконечности для отрицательных чисел. -1.3 этажей до -2, но сокращается до -1. - person Peter Cordes; 29.01.2019
comment
@markzzz: и самое главное, px = floor(...); n = int32_t(px); гарантирует, что px и n представляют одно и то же целочисленное значение, а не отличаются друг от друга на 1, если они округляются по-разному. (Например, если вы усекли LOG2E * x +0.5, чтобы получить n, но уменьшили его, чтобы получить px.) Поэтому, если ваша замена floor включает преобразование в целое число и обратно, вы должны сохранить это целочисленное значение и использовать его как n. Единственные вопросы заключаются в том, какой метод преобразования в целое вы используете и какие входные данные вы ему подаете. floor(foo + 0.5) — это неаккуратное округление к ближайшему, поэтому используйте _mm_cvtpd_epi32, а не tt. - person Peter Cordes; 29.01.2019
comment
Итак, вы предлагаете просто преобразовать double px = details::fpfloor(details::LOG2E * x + 0.5); в __m128d px = _mm_cvtepi32_pd(_mm_cvttpd_epi32(_mm_mul_pd(log2e, x)));? Но это даст отличные результаты от библиотеки, которая использует floor(x + 0.5) на github.com/dpiparo/vdt/blob/master/include/exp.h#L73 . Я понял, что ваше предложение будет лучше округляться, используя собственное округление до ближайшего, но я ищу перевод библиотеки в simd :) - person markzzz; 29.01.2019
comment
Нет, читайте внимательно. n = _mm_cvtpd_epi32(_mm_mul_pd(log2e, x))); использует ближайшее, а не _mm_cvttpd_epi32 усечение, поэтому это то же самое, что делает floor(stuff + 0.5), за исключением того, что он не является неправильным для пары угловых случаев, таких как 0.49999999999999994. См. round() для float в C++. Во всяком случае, тогда px = _mm_cvtepi32_pd(n); снова дает вам это как упакованное вдвое. (int)floor(val + 0.5) — распространенная, но плохая и немного ошибочная идиома для округления к ближайшему. Я предлагаю вам подражать этому без ошибок. - person Peter Cordes; 29.01.2019
comment
Но мне также нужно вычислить px как floor(stuff + 0.5) :) Не то чтобы это github.com/dpiparo/vdt/blob/master/include/exp.h#L77 будет использовать px, который рассчитывается с помощью floor(x + 0.5). Вот почему мне также нужно его вычислить: я не могу его игнорировать! И да, я знаю, что (int)floor(val + 0.5) is a common but bad and slightly buggy idiom for round-to-nearest. - person markzzz; 29.01.2019
comment
@markzzz: Правильно, значит, ты делаешь px = _mm_cvtepi32_pd(n), как я уже сказал по крайней мере 3 раза. Округление до int с правильным режимом, а затем обратное преобразование в double эквивалентно округлению double-›double с последующим получением версии int, как это делает скалярная версия. (Потому что вас не волнует, что произойдет с двойниками, слишком большими, чтобы поместиться в int.) - person Peter Cordes; 29.01.2019
comment
:) Но мне кажется n = _mm_cvtpd_epi32(_mm_mul_pd(log2e, x))); не Округление до int с правильным режимом. Попробуйте с value = 0.499999999999999944488848768742172978818416595458984375: floor(value + 0.5) != round(value). Я собираюсь сравнить скаляр и вектор. Я знаю, что это крайний случай, но, как я уже сказал, я хотел бы получить точно такой же :) Извините за столь педантичный... - person markzzz; 29.01.2019
comment
@markzzz: 0.49999999999999994 меньше 0,5, поэтому правильный результат округления до ближайшего равен 0, который вы получаете из _mm_cvtpd_epi32, верно? Я уже связал round() для float в C++, что объясняет, что floor(value + 0.5) не является правильно округленным результатом для этого ввода . Они отличаются, потому что это floor эмуляция round(), которая отстой, а не фактическая версия с округлением до ближайшего. (Кстати, floor(val+0.5) на самом деле не эмулирует поведение тай-брейка от нуля, как round(), я думаю, что тай-брейк идет к +Inf.) - person Peter Cordes; 29.01.2019
comment
конечно ;) Но оригинальная двойная функция Pade не использует округление до ближайшего: поэтому мое преобразование в коде simd дало бы разные результаты. Я знаю, что так будет лучше, но я не стремлюсь улучшить результаты, а получить ту же валидацию в коде simd, учитывая те же входные данные... - person markzzz; 29.01.2019
comment
@markzzz: вы действительно хотите настаивать на том, чтобы скалярная версия была совместима с ошибками / битовой точностью? Почему? Если вы хотите иметь в своем коде и то, и другое, почему бы не исправить скалярную версию и не использовать в ней rint( val )? - person Peter Cordes; 29.01.2019
comment
Я не хочу трогать исходный код; Я делаю тест на основе другой библиотеки, и мне нужно точно откалибровать производительность. Я не буду подделывать тест, заменяющий код, вот и все (даже потому, что я буду использовать его как библиотеку и т. д.). Итак, для диапазона, который я могу ввести, выполнение __m128d px = _mm_add_pd(_mm_mul_pd(log2e, x), half); __m128d t = _mm_cvtepi32_pd(_mm_cvttpd_epi32(px)); px = _mm_sub_pd(t, _mm_and_pd(_mm_cmplt_pd(px, t), one)); эквивалентно, по вашему мнению? - person markzzz; 30.01.2019
comment
@markzzz: да, если вы настаиваете на эмуляции глупого floor именно без SSE4 _mm_floor_pd, то я думаю, что эмуляция на основе усечения выглядит правильно. И это, вероятно, лучше, чем фактическое изменение режима округления MXCSR. Но почему бы для бенчмаркинга не использовать SSE4.1 _mm_floor_pd? Или вы тестируете на старых процессорах, таких как Core 2 первого поколения? - person Peter Cordes; 30.01.2019
comment
@markzzz: Предполагается ли, что cmplt_pd корректирует пол (-Inf) по сравнению с trunc (0) для отрицательных входных данных? Почему он сравнивает px и t, а не px с 0? Я предполагаю, что нужно избегать изменения значения, если оно уже было отрицательным точным целым числом? В противном случае вы можете выполнить исправление -1 / -0 в целом числе с _mm_cmpgt_epi32 перед преобразованием обратно в _pd. Тогда у вас уже есть __m128i n вместо того, чтобы снова конвертировать обратно в целое число. Кроме того, результат _mm_cmpgt_epi32 со всеми единицами/всеми нулями равен 0 или -1, так что вы можете просто добавить его. - person Peter Cordes; 30.01.2019
comment
Потому что я выпущу аудио-плагин, который должен работать и на старом ПК. В этой области SSE2 кажется правильным требованием. В любом случае код не работает. Или, по крайней мере: он возвращает те же значения четной позиции в m128d (проверьте мой обновленный ответ с последней имеющейся у меня версией). Может, эти _mm_add_epi64 _mm_set1_epi64x в конце? Я изменил его на версию epi32, все равно не работает. Эм.,... - person markzzz; 30.01.2019
comment
Да... при отладке кажется, что n становится 0.0 с помощью n = _mm_slli_epi32(n, 52); - person markzzz; 30.01.2019
comment
@markzzz: правильно, но пока вы просто сравниваете с библиотекой. Когда вы на самом деле выпускаете плагин, вы наверняка захотите использовать округление до ближайшего, потому что это быстрее и (предположительно) точнее. Хотя я предполагаю, что вы, возможно, захотите увидеть, сместилась ли минимальная/максимальная ошибка из-за того, что иногда для некоторых входных данных используются разные дробные биты. - person Peter Cordes; 30.01.2019
comment
@markzzz: я ​​обновил этот ответ некоторое время назад, указав перетасовку, необходимую для преобразования результата _mm_cvt[t]pd_epi32 в 64-битные целые числа, выровненные с вашими double. И да, сдвиги SSE насыщают счетчик сдвигов, поэтому 52-битный сдвиг на 32-битных элементах обнуляет их. Это перетасовка в конец является причиной того, что cvtpd_epi32 медленнее, чем cvtps_epi32: инструкция преобразования декодирует дополнительную операцию перетасовки. - person Peter Cordes; 30.01.2019
comment
О, я вижу. Я скучаю по этому, извините. Нужно узнать, что он на самом деле делает; пробовал на лету n = _mm_shuffle_epi32(n, _MM_SHUFFLE(3, 1, 2, 0)); n = _mm_add_epi32(n, _mm_set1_epi32(1023)); n = _mm_slli_epi32(n, 52); но не получается :) - person markzzz; 30.01.2019
comment
@markzzz: epi32 - это 32-битный размер элемента. Вы хотите epi64. Как я уже говорил, 52-битный сдвиг 32-битных элементов обнуляет их. - person Peter Cordes; 30.01.2019
comment
О, подождите, сохранение epi64, кажется, работает :) n = _mm_shuffle_epi32(n, _MM_SHUFFLE(3, 1, 2, 0)); n = _mm_add_epi64(n, _mm_set1_epi64x(1023)); n = _mm_slli_epi64(n, 52); - person markzzz; 30.01.2019
comment
@markzzz: да, настройка для _epi64 - это, очевидно, весь смысл перетасовки. - person Peter Cordes; 30.01.2019