Почему это медленнее, чем 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;
}

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

3

Другие решения

Вы можете проверить это реализация 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]);
....
3

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

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

0
По вопросам рекламы [email protected]