2013-04-13 2 views
4

Я хочу найти, сколько разных символов имеет две строки одинаковой длины. Я обнаружил, что алгоритмы xoring считаются самыми быстрыми, но они возвращают расстояние, выраженное в битах. Я хочу, чтобы результаты были выражены в символах. Предположим, что «животное» и «яма» есть расстояние 1 выражается в символах, а «е» и «я» может иметь два различных бита, так XORing возвращается 2.Охота на самую быструю реализацию Хэмминга С C

Функция я написал является:

// na = length of both strings 
unsigned int HammingDistance(const char* a, unsigned int na, const char* b) { 

    unsigned int num_mismatches = 0; 
    while (na) { 
     if (*a != *b) 
      ++num_mismatches; 

     --na; 
     ++a; 
     ++b; 
    } 

    return num_mismatches; 
} 

Может ли он стать быстрее? Возможно использование некоторых команд нижнего уровня или реализация другого алгоритма?

система: Gcc 4.7.2 на Intel Xeon X5650

Спасибо

+4

У вас возникли проблемы с производительностью, чтобы вы попытались ее оптимизировать? – Andrey

+0

xoring на отдельных персонажах может работать, так как вы хотите определить, есть ли разница между двумя символами, вы их или проверяете, что результат отличается от «0». Однако это может быть не оптимальным, если можно разработать более короткую разветвленную последовательность команд - искать ветвящиеся состояния или нераспределенный код. – didierc

+1

@ Andrey 2 Это жизненно важная часть очень интенсивного процесса. Он работает 5 миллиардов раз в моей программе. –

ответ

1

Вы можете сделать сравнение сравнить несколько байт в то время, выполнив оператор побитового на родной размер целого.

В вашем коде вы сравниваете равенство байта за раз, но ваш процессор может сравнивать хотя бы одно слово за один цикл и 8 байтов, если это x86-64. Конечно, характеристики производительности зависят от архитектуры процессора.

Но если вы продвигаетесь через два указателя с шагом 8, то в некоторых сценариях он может быть быстрее. Когда он должен читать строки из основной памяти, время загрузки памяти фактически будет доминировать над производительностью. Но если строки находятся в кэше ЦП, вы можете сделать XOR и интерпретировать результаты, проверив, где в 64-битном значении биты будут изменены.

Подсчет ведер, которые не являются 0, может быть выполнен с использованием варианта алгоритма SWAR, начиная с 0x33333333 вместо 0x55555555.

С алгоритмом будет сложнее работать, поскольку для этого потребуется использовать указатели uint64_t, которые имеют правильное выравнивание памяти. Вам понадобится преамбула и постскриптум, которые охватывают оставшиеся байты. Возможно, вы должны прочитать сборку, которую выводит компилятор, и посмотреть, не делает ли она что-то более умное, прежде чем вы попробуете что-то более сложное в коде.

+0

Я думал об этом. В то время как у него лучший лучший случай (укусы равны), средний уровень не будет значительно ниже. Особенно учитывая, что эта реализация будет значительно более сложной (esp. Alignment). – Andrey

+0

@ Andrey Да, я не могу сказать, что рекомендую попробовать. – vipw

+0

Я не пробовал это из-за сложности и относительно небольшого размера строк. В любом случае, спасибо –

1

Вместо

if (*a != *b) 
    ++num_mismatches; 

это будет быстрее на некоторых архитектурах (с 8 битными байтами), поскольку он позволяет избежать ветвей:

int bits = *a^*b; 
bits |= bits >> 4; 
bits |= bits >> 2; 
bits |= bits >> 1; 
num_mismatches += bits & 1; 
+0

Я просто попробовал это на Intel Xeon и работает медленнее, чем оригинал. Спасибо, что пытались мне помочь. –

+1

На x86 последовательность 'num_mismatches + = * a! = * B' может быть скомпилирована во что-то вроде' test% areg,% breg; setnz% al; movzbl% al,% eax; add% eax,% mismatches_reg; 'который не имеет ветвей. – fuz

1

Как о петле разворачивании:

while (na >= 8){ 
    num_mismatches += (a[0] != b[0]); 
    num_mismatches += (a[1] != b[1]); 
    num_mismatches += (a[2] != b[2]); 
    num_mismatches += (a[3] != b[3]); 
    num_mismatches += (a[4] != b[4]); 
    num_mismatches += (a[5] != b[5]); 
    num_mismatches += (a[6] != b[6]); 
    num_mismatches += (a[7] != b[7]); 
    a += 8; b += 8; na -= 8; 
} 
if (na >= 4){ 
    num_mismatches += (a[0] != b[0]); 
    num_mismatches += (a[1] != b[1]); 
    num_mismatches += (a[2] != b[2]); 
    num_mismatches += (a[3] != b[3]); 
    a += 4; b += 4; na -= 4; 
} 
if (na >= 2){ 
    num_mismatches += (a[0] != b[0]); 
    num_mismatches += (a[1] != b[1]); 
    a += 2; b += 2; na -= 2; 
} 
if (na >= 1){ 
    num_mismatches += (a[0] != b[0]); 
    a += 1; b += 1; na -= 1; 
} 

Кроме того, если вы знаете, что есть длинные отрезки одинаковых символов, вы можете наложить указатели на long* и сравнить их по 4 за раз, и только если не равный взгляд на отдельные символы. Этот код основан на memset и memcpy Быстро. Он копирует строки в массивы long, чтобы 1) устранить проблемы с выравниванием и 2) заполнить строки нулями до целого числа long. Поскольку он сравнивает каждую пару long s, если они не равны, то они направляют указатели на char* и подсчитывают неравные символы. Основная петля также может быть развернута, как описано выше.

long la[BIG_ENOUGH]; 
long lb[BIG_ENOUGH]; 
memset(la, 0, sizeof(la)); 
memset(lb, 0, sizeof(lb)); 
memcpy(la, a, na); 
memcpy(lb, b, nb); 
int nla = (na + 3) & ~3; // assuming sizeof(long) = 4 
long *pa = la, *pb = lb; 
while(nla >= 1){ 
    if (pa[0] != pb[0]){ 
    num_mismatches += (((char*)pa[0])[0] != ((char*)pb[0])[0]) 
        + (((char*)pa[0])[1] != ((char*)pb[0])[1]) 
        + (((char*)pa[0])[2] != ((char*)pb[0])[2]) 
        + (((char*)pa[0])[3] != ((char*)pb[0])[3]) 
        ; 
    } 
    pa += 1;pb += 1; nla -= 1; 
} 
+0

Я просто попробовал разворачивать свою петлю. Это медленнее, чем оригинал на Intel Xeon. У меня проблемы с реализацией второго решения. Спасибо, что помог мне в любом случае. –

+0

@Petros: Хм ... Первое решение для разворачивания не должно быть медленнее, чем ваш основной цикл символов, если, возможно, na, как правило, довольно короткий. –

+1

Новым Intel не нравится разворачиваться - по крайней мере, не всегда. Накладные расходы цикла обычно меньше, чем время перекодировки команды (из-за того, что цикл не установлен в буфере цикла). – harold

1

Если строки заполняются нулями, чтобы всегда быть 32 байта и их адреса 16 выровнены, вы могли бы сделать что-то вроде этого: (код не тестировался профилированный)

movdqa xmm0, [a] 
movdqa xmm1, [a + 16] 
pcmpeqb xmm0, [b] 
pcmpeqb xmm1, [b + 16] 
pxor xmm2, xmm2 
psadbw xmm0, xmm2 
psadbw xmm1, xmm2 
pextrw ax, xmm0, 0 
pextrw dx, xmm1, 0 
add ax, dx 
movsx eax, ax 
neg eax 

Но если строки обычно крошечные, он делает много ненужной работы, и это может быть не быстрее. Это должно быть быстрее, если строки обычно (почти) 32 байта.


редактировать: Я написал этот ответ, прежде чем я увидел обновленный комментарий - если строки, как правило, это крошечное, это, вероятно, не очень хорошо. 16-байтная версия может (возможно) быть полезной, хотя (условно выполняйте вторую итерацию, ветвь для нее должна быть хорошо предсказана, потому что она будет редко приниматься). Но с такими короткими строками нормальный код трудно превзойти.

movdqa xmm0, [a] 
pxor xmm1, xmm1 
pcmpeqb xmm0, [b] 
psadbw xmm0, xmm1 
pextrw ax, xmm0, 0 
movsx eax, ax 
neg eax 
+0

. Вся фаза заполнения должна быть слишком дорогой для моего случая, потому что она включает в себя обнуление массива из 32 байтов, а затем копирование char * до na. Я знаю, что это всего лишь 2 x memset + 2 x memcpy, но я не думаю, что перемещение и компиляция 4-31 байт будет более дорогостоящей. –

+0

@PetrosDrakoulis это позор. Я ожидал, что вес взлома будет приниматься значительно чаще, чем один раз за массив, что делает его (возможно) того стоит. – harold

+0

hm ... hamming будет вычисляться много раз (тысячи) для той же строки, но у меня нет доступа к вызывающему. Ваше решение было бы очень хорошим, если бы я мог создавать строки с номером звонящего в 32 байт и предоставлять их таким образом. В текущей реализации caller предоставляет два символа char * и длину, до которой char * гарантированно будет действительным. После этого это, вероятно, мусор, и даже если нет, я не могу этого рисковать. Хорошо, хотя хотя ... –

Смежные вопросы