Насколько быстрее строковые инструкции SSE4.2, чем SSE2 для memcmp?
Вот мой ассемблер кода
Можете ли вы встроить его в C++ и проверить по SSE4? На скорости
Мне бы очень хотелось увидеть, как шагнуло в развитие SSE4. Или его совсем не волнует? Давайте проверим (у меня нет поддержки выше SSSE3)
{ sse2 strcmp WideChar 32 bit }
function CmpSee2(const P1, P2: Pointer; len: Integer): Boolean;
asm
push ebx // Create ebx
cmp EAX, EDX // Str = Str2
je @@true // to exit true
test eax, eax // not Str
je @@false // to exit false
test edx, edx // not Str2
je @@false // to exit false
sub edx, eax // Str2 := Str2 - Str;
mov ebx, [eax] // get Str 4 byte
xor ebx, [eax + edx] // Cmp Str2 4 byte
jnz @@false // Str <> Str2 to exit false
sub ecx, 2 // dec 4
{ AnsiChar : sub ecx, 4 }
jbe @@true // ecx <= 0 to exit true
lea eax, [eax + 4] // Next 4 byte
@@To1:
movdqa xmm0, DQWORD PTR [eax] // Load Str 16 byte
pcmpeqw xmm0, DQWORD PTR [eax+edx] // Load Str2 16 byte and cmp
pmovmskb ebx, xmm0 // Mask cmp
cmp ebx, 65535 // Cmp mask
jne @@Final // ebx <> 65535 to goto final
add eax, 16 // Next 16 byte
sub ecx, 8 // Skip 8 byte (16 wide)
{ AnsiChar : sub ecx, 16 }
ja @@To1 // ecx > 0
@@true: // Result true
mov eax, 1 // Set true
pop ebx // Remove ebx
ret // Return
@@false: // Result false
mov eax, 0 // Set false
pop ebx // Remove ebx
ret // Return
@@Final:
cmp ecx, 7 // (ebx <> 65535) and (ecx > 7)
{ AnsiChar : cmp ecx, 15 }
jae @@false // to exit false
movzx ecx, word ptr @@mask[ecx * 2 - 2] // ecx = mask[ecx]
and ebx, ecx // ebx = ebx & ecx
cmp ebx, ecx // ebx = ecx
sete al // Equal / Set if Zero
pop ebx // Remove ebx
ret // Return
@@mask: // array Mersenne numbers
dw $000F, $003F, $00FF, $03FF, $0FFF, $3FFF
{ AnsiChar
dw 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383
}
end;
Семпит 32-битный https://vk.com/doc297044195_451679410
1 ответ
Вы назвали свою функцию strcmp
, но то, что вы на самом деле реализовали, требуется для выравнивания memcmp(const void *a, const void *b, size_t words)
, И то и другое movdqa
а также pcmpeqw xmm0, [mem]
произойдет сбой, если указатель не выровнен по 16B. (На самом деле, если a+4
не выровнен по 16B, потому что вы делаете первые 4 скаляра и увеличиваете их на 4 байта.)
С правильным кодом запуска и movdqu
Вы можете обрабатывать произвольные выравнивания (достигая границы выравнивания для указателя, который вы хотите использовать в качестве операнда памяти для pcmpeqw
). Для удобства вы могли бы потребовать, чтобы оба указателя были выровнены по широкому символу для начала, но вам это не нужно (особенно если вы просто возвращаете true/false, а не negative / 0 /
positive
как порядок сортировки.)
Вы спрашиваете о производительности SSE2 pcmpeqw
против pcmpistrm
, право? (Инструкции SSE4.2 явной длины, такие как pcmpestrm
Пропускная способность хуже, чем у версий с неявной длиной, поэтому используйте версии с неявной длиной в главном цикле, когда вы не близки к концу строки. Смотрите таблицы инструкций Agner Fog и руководство microarch).
Для memcmp (или тщательно реализованного strcmp) лучшее, что вы можете сделать с SSE4.2, медленнее, чем то, что вы можете сделать с SSE2 (или SSSE3) на большинстве процессоров. Может быть полезно для очень коротких строк, но не для основного цикла memcmp.
На Нехалеме: pcmpistri
равняется 4 моп, пропускная способность 2c (с операндом памяти), поэтому без дополнительных затрат цикла он может не отставать от памяти. (Nehalem имеет только 1 порт загрузки). pcmpestri
имеет пропускную способность 6с: в 3 раза медленнее.
На Песчаном мосту через Скайлэйк, pcmpistri xmm0, [eax]
имеет пропускную способность 3c, поэтому в 3 раза медленнее, чтобы не отставать от 1 вектора на такт (2 порта загрузки). pcmpestri
имеет пропускную способность 4с на большинстве из них, так что это не намного хуже. (Может быть полезно для последнего частичного вектора, но не в основном цикле).
На Сильвермонт / КНЛ, pcmpistrm
это самый быстрый и работает с пропускной способностью один на 14 циклов, так что это простой мусор для простых вещей.
На AMD Jaguar, pcmpistri
это пропускная способность 2c, так что он может быть действительно полезным (только один порт загрузки). pcmpestri
5c пропускная способность, так что это отстой.
На AMD Ризен, pcmpistri
также 2c пропускная способность, так что это дерьмо там. (2 загрузочных порта и 5 мопов за тактовую пропускную способность (или 6 мопов, если таковые имеются (или все?) Из многопользовательских инструкций) означают, что вы можете идти быстрее.
На AMD Bulldozer-family, pcmpistri
имеет пропускную способность 3c до Steamroller, где он составляет 5c. pcmpestri
имеет пропускную способность 10с. Они микрокодируются как 7 или 27 мегапикселей, поэтому AMD не тратила на них много кремния.
На большинстве процессоров они того стоят, только если вы используете их в полной мере для вещей, которые вы не можете сделать просто pcmpeq
/ pmovmskb
, Но если вы можете использовать AVX2 или особенно AVX512BW, даже выполнение сложных задач может быть быстрее с большим количеством инструкций для более широких векторов. (Более широких версий строковых инструкций SSE4.2 нет.) Может быть, строковые инструкции SSE4.2 все еще полезны для функций, которые обычно работают с короткими строками, потому что широким векторным циклам обычно требуется больше затрат на запуск / очистку. Кроме того, в программе, которая не проводит много времени в циклах SIMD, использование AVX или AVX512 в одной небольшой функции все равно снизит вашу максимальную частоту турбо тактовой частоты в течение следующей миллисекунды или около того и может легко привести к чистым потерям.
Хорошая внутренняя петля должна ограничивать пропускную способность нагрузки или подходить как можно ближе. movqdu
/ pcmpeqw [one-register]
/ pmovmskb
/ macro-fused-cmp + jcc - это всего 4 мопа с доменом слияния, так что это почти достижимо на процессорах семейства Sandybridge
См. https://www.strchr.com/strcmp_and_strlen_using_sse_4.2 для реализации и некоторых тестов, но это для строк неявной длины в стиле C, где вы должны проверить 0
байт. Похоже, вы используете строки с явной длиной, поэтому после проверки, что длины равны, это просто memcmp
, (Или я думаю, что если вам нужно найти порядок сортировки, а не просто равный / не равный, вам нужно будет записать memcmp до конца более короткой строки.)
Для strcmp с 8-битными строками на большинстве процессоров быстрее не использовать строковые инструкции SSE4.2. См. Комментарии к статье strchr.com для некоторых тестов (этой версии строки неявной длины). Например, glibc не использует строковые инструкции SSE4.2 для strcmp
потому что они не быстрее на большинстве процессоров. Они могут быть победой для strstr
хоть.
У glibc есть несколько SSE2/SSSE3 asm strcmp
а также memcmp
реализации. (Это LGPLed, так что вы не можете просто скопировать его в не-GPL проекты, но посмотрите, что они делают.) Некоторые строковые функции (например, strlen) разветвляются только на 64 байта, а затем возвращаются, чтобы разобраться какой байт в строке кэша имел удар. Но их реализация memcmp просто разворачивается с помощью movdqu / pcmpeqb
, Ты можешь использовать pcmpeqw
поскольку вы хотите знать положение первого 16-битного элемента, который отличается от первого байта.
Ваша реализация SSE2 может быть еще быстрее. Вы должны использовать режим индексированной адресации с movdqa, поскольку он не будет микросинхронизироваться с pcmpeqw (на Intel Sandybridge/Ivybridge; хорошо на Nehalem или Haswell+), но pcmpeqw xmm0, [eax]
останется в микроплавленом виде без ламинирования.
Вы должны развернуть пару раз, чтобы уменьшить накладные расходы цикла. Вы должны объединить приращение указателя со счетчиком цикла, чтобы вы cmp/jb
вместо sub/ja
: macro-fusion на большем количестве процессоров и избегает записи регистра (сокращая количество физических регистров, необходимых для переименования регистров).
Ваш внутренний цикл на Intel Sandybridge / Ivybridge будет работать
@@To1:
movdqa xmm0, DQWORD PTR [eax] // 1 uop
pcmpeqw xmm0, DQWORD PTR [eax+edx] // 2 uops on Intel SnB/IvB, 1 on Nehalem and earlier or Haswell and later.
pmovmskb ebx, xmm0 // 1 uop
cmp ebx, 65535
jne @@Final // 1 uop (macro-fused with cmp)
add eax, 16 // 1 uop
sub ecx, 8
{ AnsiChar : sub ecx, 16 }
ja @@To1 // 1 uop (macro-fused with sub on SnB and later, otherwise 2)
Это 7 мопов с плавким доменом, поэтому он может выдавать только из внешнего интерфейса в лучшем случае 7/4 циклов за итерацию на основных процессорах Intel. Это очень далеко от узких мест при 2 нагрузках за такт. В Haswell и более поздних версиях это 6/4 циклов на итерацию, потому что индексированные режимы адресации могут оставаться в микросинхронизированном состоянии с инструкцией изменения нагрузки с 2 операндами, например pcmpeqw
, но не что-нибудь еще (как pabsw xmm0, [eax+edx]
(не читает пункт назначения) или AVX vpcmpeqw xmm0, xmm0, [eax+edx]
(3 операнда). См. Режимы микросинтеза и адресации.
Это может быть более эффективным для небольших строк с лучшей настройкой / очисткой.
В вашем коде установки указателя вы можете сохранить cmp
если вы сначала проверяете NULL-указатели. Вы можете sub
/ jne
вычитать и проверять оба одинаковых с одним и тем же макросом сравнения и ветвления. (Это будет только макро-предохранитель на семействе Intel Sandybridge, и только Haswell может сделать 2 макро-слияния в одном блоке декодирования. Но процессоры Haswell/Broadwell/Skylake распространены и становятся все более распространенными, и это не имеет недостатка для других Процессоры, за исключением одинаковых указателей, настолько распространены, что проверка в первую очередь имеет значение.)
На обратном пути: всегда используйте xor eax,eax
по возможности обнулять регистр, а не mov eax, 0
,
Вы, кажется, не избегаете чтения из-за конца строки. Вы должны проверить свою функцию со строками, которые заканчиваются прямо в конце страницы, где следующая страница не отображается.
xor ebx, [eax + edx]
имеет нулевое преимущество перед cmp
для раннего скалярного теста. cmp/jnz
может макро слиться с jcc, но xor
не может.
Вы загружаете маску для обработки очистки, чтобы покрыть случай, когда вы читаете за концом строки. Вы, вероятно, все еще можете использовать обычный bsf
найти первое отличие в растровом изображении. Я думаю, инвертировать это с not
найти первую позицию, которая не сравнивается, и убедиться, что она меньше, чем оставшаяся длина строки.
Или вы можете создать маску на лету с mov eax, -1
а также shr
, Я думаю. Или для загрузки, вы можете иногда использовать скользящее окно в ...,0,0,0,-1,-1,-1,...
массив, но вам нужны суббайтовые смещения, чтобы это не работало. (Это хорошо работает для векторных масок, если вы хотите замаскировать и повторить pmovmskb
, Векторизация с невыровненными буферами: использование VMASKMOVPS: создание маски из числа смещений? Или вообще не использовать этот insn).
Ваш путь не плох, если он не кэширует промах. Я бы, наверное, пошел на создание маски на лету. Может быть, до цикла в другом регистре, потому что вы можете замаскировать, чтобы получить count % 8
, поэтому генерация маски может происходить параллельно с циклом.