Как эффективно комбинировать сравнения в 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. Если(...) оператор оценивает 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 в этом слоте), как могут результаты всех сравнений быть (например) и-ЕД или-ЕД вместе, в целом? Is 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.

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

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

2 ответов


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

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


чтобы обратиться к вашей отредактированной версии:

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

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

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

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