Я пытаюсь сравнить два ряда pixel
s.
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
и все флаги оптимизации.
Я написал оптимизацию 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 — простая маленькая функция, вы легко разберетесь со всеми хитростями реализации.