Почему это медленнее, чем memcmp

Я пытаюсь сравнить два ряда pixels.

pixel определяется как struct содержащий 4 float значения (RGBA).

Причина, по которой я не пользуюсь memcmp потому что мне нужно вернуть положение 1-го другого пикселя, который memcmp не делает.

Моя первая реализация использует SSE и примерно на 30% медленнее, чем memcmp:

inline int PixelMemCmp(const Pixel* a, const Pixel* b, int count)
{
    for (int i = 0; i < count; i++)
    {
        __m128 x = _mm_load_ps((float*)(a + i));
        __m128 y = _mm_load_ps((float*)(b + i));
        __m128 cmp = _mm_cmpeq_ps(x, y);
        if (_mm_movemask_ps(cmp) != 15) return i;
    }
    return -1;
}

Затем я обнаружил, что обработка значений как целых чисел, а не чисел с плавающей запятой, немного ускорила процесс, и теперь это только на ~20% медленнее, чем memcmp,

inline int PixelMemCmp(const Pixel* a, const Pixel* b, int count)
{
    for (int i = 0; i < count; i++)
    {
        __m128i x = _mm_load_si128((__m128i*)(a + i));
        __m128i y = _mm_load_si128((__m128i*)(b + i));
        __m128i cmp = _mm_cmpeq_epi32(x, y);
        if (_mm_movemask_epi8(cmp) != 0xffff) return i; 
    }
    return -1;
}

Из того, что я читал по другим вопросам, реализация MS memcmp также реализуется с помощью SSE, У меня вопрос, какие еще хитрости есть у реализации MS в рукаве, чего нет у меня? Как это все еще быстрее, даже если он делает побайтовое сравнение?

Является ли выравнивание проблемой? Если pixel содержит 4 числа с плавающей запятой, не будет ли массив пикселей уже размещен на границе 16 байт?

Я собираю с /o2 и все флаги оптимизации.

3 ответа

Решение

Я написал оптимизацию strcmp/memcmp с SSE (и MMX/3DNow!), И первый шаг - убедиться, что массивы выровнены как можно более точно - вы можете обнаружить, что вам нужно выполнить первый и / или последний байт "один вовремя".

Если вы можете выровнять данные до того, как они попадут в цикл [если ваш код выполняет распределение], то это идеально.

Вторая часть состоит в том, чтобы развернуть цикл, чтобы вы не получили так много "если цикл не в конце, вернитесь к началу цикла" - при условии, что цикл довольно длинный.

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

Изменить: последний абзац может понадобиться пример. Этот код предполагает развертывание цикла как минимум из двух:

 __m128i x = _mm_load_si128((__m128i*)(a));
 __m128i y = _mm_load_si128((__m128i*)(b));

 for(int i = 0; i < count; i+=2)
 {
    __m128i cmp = _mm_cmpeq_epi32(x, y);

    __m128i x1 = _mm_load_si128((__m128i*)(a + i + 1));
    __m128i y1 = _mm_load_si128((__m128i*)(b + i + 1));

    if (_mm_movemask_epi8(cmp) != 0xffff) return i; 
    cmp = _mm_cmpeq_epi32(x1, y1);
    __m128i x = _mm_load_si128((__m128i*)(a + i + 2));
    __m128i y = _mm_load_si128((__m128i*)(b + i + 2));
    if (_mm_movemask_epi8(cmp) != 0xffff) return i + 1; 
}

Примерно как то так.

Возможно, вы захотите проверить эту реализацию memcmp SSE, в частности __sse_memcmp функция, она начинается с некоторых проверок работоспособности, а затем проверяет, выровнены ли указатели:

aligned_a = ( (unsigned long)a & (sizeof(__m128i)-1) );
aligned_b = ( (unsigned long)b & (sizeof(__m128i)-1) );

Если они не выровнены, он сравнивает байты указателя до начала выровненного адреса:

 while( len && ( (unsigned long) a & ( sizeof(__m128i)-1) ) )
{
   if(*a++ != *b++) return -1;
   --len;
}

А затем сравнивает оставшуюся память с инструкциями SSE, аналогичными вашему коду:

 if(!len) return 0;
while( len && !(len & 7 ) )
{
__m128i x = _mm_load_si128( (__m128i*)&a[i]);
__m128i y = _mm_load_si128( (__m128i*)&b[i]);
....

Я не могу помочь вам напрямую, потому что я использую Mac, но есть простой способ выяснить, что происходит:

Вы просто входите в memcpy в режиме отладки и переключаетесь в представление "Разборка". Поскольку memcpy - простая маленькая функция, вы легко разберетесь со всеми хитростями реализации.

Другие вопросы по тегам