2016-12-24 26 views
4

1) Есть ли способ эффективно реализовать sign function с использованием SSE3 (без SSE4) со следующими характеристиками?Как реализовать функцию знака с помощью SSE3?

  • Ввод представляет собой поплавковый вектор __m128.
  • выход должен быть также __m128 с [-1.0f, 0.0f, 1.0f] в качестве его значения

Я попытался это, но он не работает (хотя я думаю, что он должен):

inputVal = _mm_set_ps(-0.5, 0.5, 0.0, 3.0); 
comp1 = _mm_cmpgt_ps(_mm_setzero_ps(), inputVal); 
comp2 = _mm_cmpgt_ps(inputVal, _mm_setzero_ps()); 
comp1 = _mm_castsi128_ps(_mm_castps_si128(comp1)); 
comp2 = _mm_castsi128_ps(_mm_castps_si128(comp2)); 
signVal = _mm_sub_ps(comp1, comp2); 

2) есть ли способ, чтобы создать функцию «флаг» (я не уверен, о правильном названии). А именно, если A > B результат будет 1 и 0 в противном случае. Результат должен быть с плавающей точкой (__m128), как и его ввод.

UPDATE: Кажется, Кори Нельсон ответ будет работать здесь:

__m128 greatherThanFlag = _mm_and_ps(_mm_cmpgt_ps(valA, valB), _mm_set1_ps(1.0f));  
__m128 lessThanFlag = _mm_and_ps(_mm_cmplt_ps(valA, valB), _mm_set1_ps(1.0f)); 
+0

Для части 2, почему вы хотите 'флаг 1.0f', вместо того, чтобы просто и маска из' _mm_cmpgt_ps'? например чтобы условно добавить что-то, замаскируйте операнд с результатом сравнения с нулевыми элементами, где сравнение является ложным. all-zero bits = 0.0f, который является аддитивным тождеством. Или если вы просто хотите сделать что-то 0 или без изменений, И это с маской. –

+0

@PeterCordes, я не уверен, что понимаю вас (я не так разбираюсь в битах :-)). Вы могли бы написать решение для части 2? Чем эффективнее, тем лучше :-). – Royi

+0

Если это то, что вы хотите, это самый эффективный способ получить его. Но я хочу сказать, что получение результата «0.0» или «1.0», вероятно, не всегда полезно. Что вы хотите с этим делать? –

ответ

4

Первое, что приходит на ум, это, возможно, самый простой:

__m128 sign(__m128 x) 
{ 
    __m128 zero = _mm_setzero_ps(); 

    __m128 positive = _mm_and_ps(_mm_cmpgt_ps(x, zero), _mm_set1_ps(1.0f)); 
    __m128 negative = _mm_and_ps(_mm_cmplt_ps(x, zero), _mm_set1_ps(-1.0f)); 

    return _mm_or_ps(positive, negative); 
} 

Или, если вы оговорился и предназначены, чтобы получить целочисленный результат:

__m128i sign(__m128 x) 
{ 
    __m128 zero = _mm_setzero_ps(); 

    __m128 positive = _mm_and_ps(_mm_cmpgt_ps(x, zero), 
           _mm_castsi128_ps(_mm_set1_epi32(1))); 
    __m128 negative = _mm_cmplt_ps(x, zero); 

    return _mm_castps_si128(_mm_or_ps(positive, negative)); 
} 
+0

Кори Нельсон, спасибо. +1 и правильный ответ. – Royi

+0

Вы можете сделать слегка-неаккуратную версию этого, используя только один '_mm_cmpneq_ps (x, zero)', а затем скопируйте бит знака. См. Мой ответ для версии, которая возвращает '-0.0', когда ввод' -0.0', но в противном случае возвращает то же самое для не-NaN-входов. –

+0

Как указывает целочисленная версия плазмаселя, результат сравнения «все-теги» уже является целым числом «-1». Компилятор, надеюсь, оптимизирует «_mm_and_ps» с помощью 'set1_epi32 (-1)'. И интересно, вы можете превратить результат сравнения в целое число 0 или 1 с логическим сдвигом вправо, так что вам также не нужна константа. (Но у этого актера, скорее всего, будет байпас-задержка). В любом случае, вы должны использовать '_mm_and_si128' и' _mm_or_si128' в своей целочисленной версии. –

2

Вы близки, но ваш код не совсем работает, потому что вы пытаетесь преобразовать 0/-1 Int к float, используя только отливку.

Попробуйте это (непроверенные):

inputVal = _mm_set_ps(-0.5, 0.5, 0.0, 3.0); 
comp1 = _mm_cmpgt_ps(_mm_setzero_ps(), inputVal); 
comp2 = _mm_cmpgt_ps(inputVal, _mm_setzero_ps()); 
comp1 = _mm_cvtepi32_ps(_mm_castps_si128(comp1)); // 0/-1 => 0.0f/-1.0f 
comp2 = _mm_cvtepi32_ps(_mm_castps_si128(comp2)); 
signVal = _mm_sub_ps(comp1, comp2); 

Сказав, что, я думаю, Cory's solution, вероятно, более эффективным.

+0

В нем говорится «Нет подходящего пользовательского преобразования от _m128 до _mm128i. – Royi

+0

Ah - некоторым компиляторам понадобится листинг (в компиле вы используете BTW ?) - Я исправлю это в ближайшее время. –

+0

Я использую VS 2015. – Royi

3

Если вам нужен signum function для float векторов, где результатом является вектор int32_t, и вам не все равно NaN s, тогда более эффективная версия может быть реализована с использованием целочисленных инструкций на основе следующей теории.

Если взять число с плавающей запятой и переосмысливать биты в подписанную двойки дополнение целое число, вы можете получить 3 различных случая (где X произвольная 0 или 1, а жирный MSB бит знака) :

  • 0X X X X X X X X X X X X X X 1, который является > 0 (или > 0.0f, как поплавок)
  • 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0, который является == 0 (или == 0.0f, как поплавок)
  • 1X X X X X X X X X X X X X X X, который является < 0 (или <= 0.0f, как поплавок)

Последний случай является неоднозначным, так как это может быть специальный с плавающей точкой случай отрицательного нулевого -0.0f:

  • 10 0 0 0 0 0 0 0 0 0 0 0 0 0 0, который является == -0.0f == 0.0f as float

С этого момента функция signum с плавающей запятой превращается в целочисленную функцию.


Используя встроенные функции, доступные с SSE3 (не SSSE3) это может быть реализована как:

inline __m128i _mm_signum_ps(__m128 a) 
{ 
    __m128i x = _mm_castps_si128(a); 

    __m128i zero = _mm_setzero_si128(); 
    __m128i m0 = _mm_cmpgt_epi32(x, zero); 
    __m128i m1 = _mm_cmplt_epi32(x, zero); 
    __m128i m2 = _mm_cmpeq_epi32(x, _mm_set1_epi32(0x80000000)); 

    __m128i p = _mm_and_si128(m0, _mm_set1_epi32(+1)); 
    __m128i n = _mm_and_si128(m1, _mm_set1_epi32(-1)); 

    return _mm_andnot_si128(m2, _mm_or_si128(p, n)); 
} 

Оптимизированная версия этого

inline __m128i _mm_signum_ps(__m128 a) 
{ 
    __m128i x = _mm_castps_si128(a); 

    __m128i zr = _mm_setzero_si128(); 
    __m128i m0 = _mm_cmpeq_epi32(x, _mm_set1_epi32(0x80000000)); 
    __m128i mp = _mm_cmpgt_epi32(x, zr); 
    __m128i mn = _mm_cmplt_epi32(x, zr); 

    return _mm_or_si128(
     _mm_andnot_si128(m0, mn), 
     _mm_and_si128(mp, _mm_set1_epi32(1)) 
    ); 
} 

Как Петр предложил в комментариях, используя одно сравнение с плавающей запятой _mm_cmplt_ps вместо двух целых сравнений _mm_cmplt_epi32/_mm_cmpeq_epi32 для обработки -0.0f save s 1, но он может страдать от задержки задержки при переходе из-за переключения между доменами с плавающей запятой/целым числом, поэтому, возможно, лучше придерживаться только целочисленной реализации выше. Или нет. Поскольку вам нужен целочисленный результат, более вероятно, что вы будете использовать его и поменять на целочисленный домен в любом случае. Итак:

inline __m128i _mm_signum_ps(__m128 a) 
{ 
    __m128i x = _mm_castps_si128(a); 
    __m128 zerops = _mm_setzero_ps(); 

    __m128i mn = _mm_castps_si128(_mm_cmplt_ps(a, zerops)); 
    __m128i mp = _mm_cmpgt_epi32(x, _mm_castps_si128(zerops)); 

    return _mm_or_si128(mn, _mm_and_si128(mp, _mm_set1_epi32(1))); 
} 

С -march=x86-64 -msse3 -O3 в звоне 3.9 это компилируется

_mm_signum_ps(float __vector(4)):    # @_mm_signum2_ps(float __vector(4)) 
     xorps xmm1, xmm1      # fp domain 
     movaps xmm2, xmm0      # fp domain 
     cmpltps xmm2, xmm1      # fp domain 
     pcmpgtd xmm0, xmm1      # int domain 
     psrld xmm0, 31       # int domain 
     por  xmm0, xmm2      # int domain 
     ret 

Кроме cmpltps, задержка каждой команды здесь 1 с пропускными <= 1. Я думаю, что это действительно эффективное решение, и его можно улучшить с помощью SSSE3's _mm_sign_epi32.


Если вам нужны результаты с плавающей точкой, то лучше остаться полностью в плавающей точке области (вместо замены между плавающей точкой/целыми областями), так что используйте один из Peter's solutions.

+0

Я думаю, что если вы хотите получить целочисленный результат без SSSE3 'psignd', возможно, быстрее использовать cmpps, а не специальный случай' -0.0'. Тем не менее использование сдвигов вместо (или после) - это аккуратный трюк и сохраняет некоторые целочисленные константы. –

+0

Не знаю, о чем вы говорите, желая, чтобы компилятор использовал инструкции 'pcmpgtd' для сравнения с нулем. На Haswell 'psrad' работает на p0, а' pcmpgtd' - на p15. Можете ли вы объяснить, почему вы думаете, что здесь есть какой-то потерянный параллелизм на уровне инструкций? Я не смотрел супер-близко, так что, может быть, есть что-то, и в этом случае вы должны объяснить это в своем ответе. –

+0

@PeterCordes Нет, вы правы. Я неправильно думал, что они работают на одних и тех же портах. На этапе сравнения я сосредоточился на выполнении 2x'pcmpgtd' параллельно на одном и том же порту, оставив остальные порты свободными. В этом случае обе скомпилированная версия эквивалентна производительности, если она не отображает p0 из другой команды. – plasmacel

4

Если это нормально для sgn(-0.0f), для получения результата -0.0f вместо +0.0f, вы можете сохранить инструкцию или два по сравнению с версией @Cory Nelson. См. Ниже версию, которая также распространяет NaN.

  • выбрать 0,0 или 1,0 на основе сравнения для x != 0.0f
  • скопировать знаковый бит x в этом.

// return -0.0 for x=-0.0, otherwise the same as Cory's (except for NaN which neither handle well) 
__m128 sgn_fast(__m128 x) 
{ 
    __m128 negzero = _mm_set1_ps(-0.0f); 

    // using _mm_setzero_ps() here might actually be better without AVX, since xor-zeroing is as cheap as a copy but starts a new dependency chain 
    //__m128 nonzero = _mm_cmpneq_ps(x, negzero); // -0.0 == 0.0 in IEEE floating point 
    __m128 nonzero = _mm_cmpneq_ps(x, _mm_setzero_ps()); 

    __m128 x_signbit = _mm_and_ps(x, negzero); 

    __m128 zeroone = _mm_and_ps(nonzero, _mm_set1_ps(1.0f)); 
    return _mm_or_ps(zeroone, x_signbit); 
} 

Когда вход NaN, я думаю, что она возвращает +/- 1.0f, по знаку NaN. (Так как _mm_cmpneq_ps() истинно, когда x является NaN: см. the table on the CMPPD instruction).

Без AVX это на два меньше инструкций, чем версия Cory (with clang3.9 on the Godbolt compiler explorer). При вставке в цикл операнды источника памяти могут быть регистровыми операндами. gcc использует больше инструкций, делая отдельную нагрузку MOVAPS и сама покраску в угол, который требует дополнительного MOVAPS для получения возвращаемого значения в xmm0.

xorps xmm1, xmm1 
    cmpneqps  xmm1, xmm0 
    andps xmm0, xmmword ptr [rip + .LCPI0_0] # x_signbit 
    andps xmm1, xmmword ptr [rip + .LCPI0_1] # zeroone 
    orps xmm0, xmm1 

Задержка критического пути является cmpneqps + andps + orps, что 3 + 1 + 1 циклов на Intel Haswell для примера. Версия Cory должна запускать две инструкции cmpps параллельно для достижения этой задержки, что возможно только на Skylake. Другие процессоры будут иметь конфликт ресурсов, вызывающий дополнительный цикл задержки.


Для распространения NaN, поэтому возможные выходы бы -1.0f, -/+0.0f, 1.0f и NaN, мы могли бы воспользоваться тем фактом, что все-онов немного шаблон является NaN.

  • _mm_cmpunord_ps(x,x) для получения NaN-маски. (Или, что то же самое, cmpneqps)
  • or что на результат оставить его неизменным или заставить его NaN.

// return -0.0 for x=-0.0. Return -NaN for any NaN 
__m128 sgn_fast_nanpropagating(__m128 x) 
{ 
    __m128 negzero = _mm_set1_ps(-0.0f); 
    __m128 nonzero = _mm_cmpneq_ps(x, _mm_setzero_ps()); 

    __m128 x_signbit = _mm_and_ps(x, negzero); 
    __m128 nanmask = _mm_cmpunord_ps(x,x); 
    __m128 x_sign_or_nan = _mm_or_ps(x_signbit, nanmask); // apply it here instead of to the final result for better ILP 

    __m128 zeroone = _mm_and_ps(nonzero, _mm_set1_ps(1.0f)); 
    return _mm_or_ps(zeroone, x_sign_or_nan); 
} 

компилируется эффективно, и едва удлиняет критическую задержку пути. Однако требуется больше инструкций MOVAPS для копирования регистров без AVX.


Вы могли бы быть в состоянии сделать что-то полезное с SSE4.1 BLENDVPS, но это не самый эффективный инструкции на всех процессорах. Также трудно избежать обработки отрицательного нуля как ненулевого.


Если вы хотите целочисленный результат, вы можете использовать SSSE3 _mm_sign_epi32(set1(1), x) получить -1, 0 или 1 выход.Если -0.0f -> -1 слишком неаккуратно, вы можете исправить это вверх с операции AND результате _mm_cmpneq_ps(x, _mm_setzero_ps())

// returns -1 for x = -0.0f 
__m128i sgn_verysloppy_int_ssse3(__m128 x) { 
    __m128i one = _mm_set1_epi32(1); 
    __m128i sign = _mm_sign_epi32(one, _mm_castps_si128(x)); 
    return sign; 
} 

// correct results for all inputs 
// NaN -> -1 or 1 according to its sign bit, never 0 
__m128i sgn_int_ssse3(__m128 x) { 
    __m128i one = _mm_set1_epi32(1); 
    __m128i sign = _mm_sign_epi32(one, _mm_castps_si128(x)); 

    __m128 nonzero = _mm_cmpneq_ps(x, _mm_setzero_ps()); 
    return _mm_and_si128(sign, _mm_castps_si128(nonzero)); 
} 
+0

Я придумал аналогичный ответ для целочисленных результатов, но без SSSE3. – plasmacel

+0

Отличный ответ! Я +1 это. Я не против '-0.0f', чтобы стать' -0.0f'. Но это не может быть '-1f'. Как бы вы сделали вторую часть наиболее эффективно? Что делать, если SSE3 разрешен? – Royi

+1

Я только что понял, что для '-0.0f' ваше решение производит' -0.0f', а не '-1.0f'. Если результат будет использоваться в операциях с плавающей запятой, это не хуже, чем решение Cory.Более того, он сохраняет знак всех значений с плавающей запятой, одновременно создавая правильные математические результаты для знака. На самом деле, это более совместимое с IEEE-754 решение, мне оно нравится. – plasmacel