Насколько быстрее строковые инструкции 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, поэтому генерация маски может происходить параллельно с циклом.

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