Если вы хотите оптимизировать для практически идентичных строк, вы можете использовать векторные инструкции x86 SSE/AVX. Ваша основная идея выглядит отлично: перерыв, как только вы обнаружите второе различие.
Чтобы найти и сравнить отличия символов, такая последовательность, как PCMPEQB
/PMOVMSKB
/test-and-branch, вероятно, хороша. (Используйте встроенные функции C/C++ для получения этих векторных инструкций). Когда ваш векторный цикл обнаруживает ненулевые различия в текущем блоке, POPCNT
битмаски, чтобы увидеть, только что вы обнаружили первое различие или обнаружили две отличия в одном блоке.
Я объединил непроверенную и не совсем полную версию AVX2 того, что я описываю. Этот код предполагает, что длина строк кратно 32. Остановка на ранней стадии и обработка последнего фрагмента с эпилогом очистки - это упражнение для читателя.
#include <immintrin.h>
#include <string>
// not tested, and doesn't avoid reading past the end of the string.
// TODO: epilogue to handle the last up-to-31 left-over bytes separately.
bool canChange_avx2_bmi(std::string const& strA, std::string const& strB, char& letter) {
size_t size = strA.size();
if (size != strB.size())
return false;
int diffs = 0;
size_t diffpos = 0;
size_t pos = 0;
do {
uint32_t diffmask = 0;
while(pos < size) {
__m256i vecA = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(& strA[pos]));
__m256i vecB = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(& strB[pos]));
__m256i vdiff = _mm256_cmpeq_epi8(vecA, vecB);
diffmask = _mm256_movemask_epi8(vdiff);
pos += 32;
if (diffmask) break; // gcc makes worse code if you include && !diffmask in the while condition, instead of this break
}
if (diffmask) {
diffpos = pos + _tzcnt_u32(diffmask); // position of the lowest set bit. Could safely use BSF rather than TZCNT here, since we only run when diffmask is non-zero.
diffs += _mm_popcnt_u32(diffmask);
}
} while(pos < size && diffs <= 1);
if (diffs == 1) {
letter = strA[diffpos];
return true;
}
return false;
}
Уродливый break
вместо включения, что в состоянии while
по-видимому, помогает gcc generate better code. do{}while()
также соответствует тому, как я хочу, чтобы asm вышел. Я не пробовал использовать цикл for
или while
, чтобы узнать, что сделает gcc.
Внутренний цикл действительно туго так:
.L14:
cmp rcx, r8
jnb .L10 # the while(pos<size) condition
.L6: # entry point for first iteration, because gcc duplicates the pos<size test ahead of the loop
vmovdqu ymm0, YMMWORD PTR [r9+rcx] # tmp118,* pos
vpcmpeqb ymm0, ymm0, YMMWORD PTR [r10+rcx] # tmp123, tmp118,* pos
add rcx, 32 # pos,
vpmovmskb eax, ymm0 # tmp121, tmp123
test eax, eax # tmp121
je .L14 #,
В теории, это должно работать на одной итерации в 2 часах (Intel Haswell). В цикле есть 7 совпадающих доменов. (Будет 6, но 2-reg addressing modes apparently can't micro-fuse on SnB-family CPUs.) Поскольку два из uops - это нагрузки, а не ALU, эта пропускная способность может быть достижима на SnB/IvB.
Это должно быть исключительно хорошо для полета над областями, где две строки идентичны. Накладные расходы на правильную обработку произвольной длины строки сделают это потенциально медленнее, чем простая скалярная функция, если строки коротки и/или имеют несколько различий на ранней стадии.
какую оптимизацию вы имеете в виду? время? Память? кстати, вы не обратились к индексу персонажа, который был изменен, вы просто возвращаете сам символ – Pooya
Я могу видеть 'canChange()' (смешное имя, при этом) проверку пар символов - можете ли вы подробнее описать ' каждая пара слов?? – greybeard
Я думаю, что это «итерация через каждую пару слов», которая является источником медленности; опубликованный код выглядит довольно эффективно. Если вы сравниваете каждое слово в тексте 1 с каждым словом в тексте 2, это операция O (N^2), и она будет медленной, независимо от того, насколько вы оптимизируете canChange(). –