Как эффективно сочетать сравнения в SSE?

Я пытаюсь преобразовать следующий код в SSE/AVX:

float x1, x2, x3;
float a1[], a2[], a3[], b1[], b2[], b3[];
for (i=0; i < N; i++)
{
    if (x1 > a1[i] && x2 > a2[i] && x3 > a3[i] && x1 < b1[i] && x2 < b2[i] && x3 < b3[i])
    {
        // do something with i
    }
}

Здесь N - малая константа, скажем 8. Оператор if (...) больше всего принимает значение false.

Первая попытка:

__m128 x; // x1, x2, x3, 0
__m128 a[N]; // packed a1[i], a2[i], a3[i], 0 
__m128 b[N]; // packed b1[i], b2[i], b3[i], 0

for (int i = 0; i < N; i++)
{
    __m128 gt_mask = _mm_cmpgt_ps(x, a[i]);
    __m128 lt_mask = _mm_cmplt_ps(x, b[i]);
    __m128 mask = _mm_and_ps(gt_mask, lt_mask);
    if (_mm_movemask_epi8 (_mm_castps_si128(mask)) == 0xfff0)
    {
        // do something with i
    }
}

Это работает и достаточно быстро. Вопрос в том, есть ли более эффективный способ сделать это? В частности, если есть регистр с результатами сравнений SSE или AVX при поплавках (которые помещают 0xffff или 0x0000 в этот слот), как могут быть результаты всех сравнений (например) и-ed или или - вместе, в общем? Является ли PMOVMSKB (или соответствующий _mm_movemask intrinsic) стандартный способ сделать это?

Кроме того, как можно использовать 256-битные регистры AVX вместо SSE в коде выше?

EDIT:

Протестировано и сравнили версию с использованием VPTEST (от _mm_test * intrinsic), как предложено ниже.

__m128 x; // x1, x2, x3, 0
__m128 a[N]; // packed a1[i], a2[i], a3[i], 0
__m128 b[N]; // packed b1[i], b2[i], b3[i], 0
__m128i ref_mask = _mm_set_epi32(0xffff, 0xffff, 0xffff, 0x0000);

for (int i = 0; i < N; i++)
{
    __m128 gt_mask = _mm_cmpgt_ps(x, a[i]);
    __m128 lt_mask = _mm_cmplt_ps(x, b[i]);
    __m128 mask = _mm_and_ps(gt_mask, lt_mask);
    if (_mm_testc_si128(_mm_castps_si128(mask), ref_mask))
    {
        // do stuff with i
    }
}

Это также работает и быстро. Бенчмаркинг (Intel i7-2630QM, Windows 7, cygwin 1.7, cygwin gcc 4.5.3 или mingw x86_64 gcc 4.5.3, N = 8) показывает, что это идентичная скорость для кода выше (менее 0,1%) на 64-битной, Любая версия внутреннего цикла работает примерно в 6,8 часах в среднем по данным, которые все находятся в кеше и для которых всегда возвращается значение false.

Интересно, что на 32-битной версии версия _mm_test работает на 10% медленнее. Оказывается, компилятор проливает маски после разворота цикла и должен перечитать их обратно; это, вероятно, не нужно, и его можно избежать в сборке с ручным кодированием.

Какой метод выбрать? Похоже, что нет веских причин предпочитать VPTEST над VMOVMSKPS. На самом деле, есть небольшая причина, чтобы предпочесть VMOVMSKPS, а именно, освобождает регистр xmm, который в противном случае был бы взят маской.

Ответ 1

Если вы работаете с поплавками, вы обычно хотите использовать MOVMSKPS (и соответствующую инструкцию AVX VMOVMSKPS) вместо PMOVMSKB.

В стороне, да, это один из стандартных способов сделать это; вы также можете использовать PTEST (VPTEST) для непосредственного обновления флагов условий на основе результата SSE или AVX AND или ANDNOT.

Ответ 2

Чтобы обратиться к редактируемой версии:

Если вы собираетесь напрямую подключиться к результату PTEST, быстрее использовать его, чем MOVMSKPS, в реестр GP, а затем сделать TEST, чтобы установить флаги для ветки инструкция. На процессорах AMD перемещение данных между векторными и целыми доменами происходит очень медленно (от 5 до 10 циклов в зависимости от модели ЦП).

Если вам нужен дополнительный регистр для PTEST, вы часто этого не делаете. Вы можете использовать то же значение, что и оба аргумента, например, с обычной не-векторной командой TEST. (Тестирование foo & foo совпадает с тестом foo).

В вашем случае вам нужно проверить, что все векторные элементы установлены. Если вы измените сравнение, а затем ИЛИ результат вместе (так что вы тестируете !(x1 < a1[i]) || !(x2 < a2[i]) || ...), у вас будут векторы, необходимые для тестирования для всех нулей, а не для всех. Но проблема с низким элементом все еще проблематична. Если вам нужно сохранить регистр, чтобы избежать необходимости использовать векторную маску для PTEST/VTESTPS, вы могли бы сдвинуть вектор на 4 байта перед тем, как сделать PTEST, и разветвление на нем будет все-ноль.

AVX представил VTESTPS, который, как я полагаю, позволяет избежать возможной задержки байпаса float → int. Если вы использовали какие-либо инструкции внутри домена для генерации входных данных для теста, вы могли бы также использовать (V)PTEST. (Я знаю, что вы используете внутренности, но им больно печатать и смотреть по сравнению с мнемониками.)