English | Русский

Блог о Linux и велосипедах

Adler32 как кольцевой хеш

Что такое кольцевой1 хеш?

Основная особенность кольцевого хеша в том, что вычисление следующего значения хеш-суммы для блока данных, сдвинутого на один или более байт, производится на основании предыдущего значения хеш-суммы, вместо того, чтобы вычислять его заново. Это способствует значительному ускорению вычислений для задач, которым необходимо вычислять хеш-суммы для каждого байтового (или более) смещения. Такой хеш используется в rsync, системах блочной дедупликации данных, алгоритме Рабина — Карпа (поиск подстроки в строке).

Алгоритм Adler32

Одна из самых простых хеш-функций, которую можно придумать — сумма всех байт (или слов) по какому-то модулю (например, значению, ограниченному разрядной сеткой). Несмотря на высокую производительность, такая хеш-сумма дает много коллизий. И объяснение тому просто: «От перемены мест слагаемых сумма не меняется». Алгоритм Adler'а почти настолько же прост, только учитывает расположение слагаемых. Хеш-сумма Adler32 состоит из двух 16-разрядных слов. Первое слово — это сумма всех байт по модулю. Второе — сумма значений первого слова на каждой итерации по этому же модулю.

Если длина входной последовательности — N, то второе слово можно представить как сумму произведений i-го байта на (N - i + 1). Например, для последовательности 1, 2, 3, второе слово будет равным 1 + (1 + 2) + (1 + 2 + 3), или же 1•3 + 2•2 + 3•1. Таким образом, если переставить два байта во входной последовательности, то сумма поменяется из-за изменения коэффициентов при слагаемых.

Модуль для Adler32 выбран как наибольшее простое число, которое меньше 216. Пока я тут это пишу, читатель, должно быть, уже успел прикинуть, что это число — 65521. Такой выбор должен улучшить вероятностные характеристики хеш-функции. Начальное значение суммы (первого слова) — 1.

Следует заметить, что данный хеш не является достаточно надежным. Вероятность коллизий у него даже выше, чем у CRC32. При этом Adler32 быстрее CRC32 в несколько раз. Это может дать преимущество в задачах, в которых после сравнения хеша на равенство в случае успеха производится более надежное сравнение (например, по криптографическому хешу или побайтовое сравнение).

Прямая реализация алгоритма выглядит следующим образом:

uint32_t adler32(unsigned char *data, int len){

    uint32_t sum1 = 1;
    uint32_t sum2 = 0;

    for (i = 0; i < len; i++){
        sum1 = (sum1 + buf[i]) % BASE;
        sum2 = (sum2 + sum1) % BASE;
    }

    return (sum2 << 16) | sum1;
}

Более эффективная реализация алгоритма Adler32 из библиотеки zlib:

uint32_t adler32(uint32_t adler, const unsigned char* buf,
                 unsigned len){

    uint32_t sum2;
    unsigned n;

    /* Обработка частных случаев...*/

    /* do length NMAX blocks -- requires just one modulo operation */
    while (len >= NMAX) {
        len -= NMAX;
        n = NMAX / 16;          /* NMAX is divisible by 16 */
        do {
            DO16(buf);          /* 16 sums unrolled */
            buf += 16;
        } while (--n);
        adler %= BASE;
        sum2 %= BASE;
    }

    /* Обработка оставшихся элементов массива...*/

    /* return recombined sums */
    return adler | (sum2 << 16);
}

Основная оптимизация здесь — это выполнения операции остатка от деления каждые NMAX байт вместо одного. NMAX — это максимальное число, при котором не происходит переполнение sum2. Вычислить его можно, исходя из того, что sum2 является суммой арифметической прогрессии с разностью d=255 (берем наихудший случай). Так, при 32-разрядном представлении чисел, получаем

(1 • 2 + 255 • (NMAX - 1)) • NMAX/2 = 232 - 1

Открыв исходный файл решая квадратное уравнение, получаем NMAX = 5552 с учетом того, что оно должно быть кратно 16.

Макрос DO16(buf) разворачивается в следующее выражение

adler += buf[0]; sum2 += adler; adler += buf[1]; sum2 += adler; ...
adler += buf[15]; sum2 += adler;

Данная реализация алгоритма работает в 5-6 раз быстрее, чем реализация «в лоб».

Превращаем Adler32 в кольцевой хеш

Как изменится хеш-сумма, если из последовательности байт убрать первый, а в конец добавить новый? Очевидно, что первое слово «увеличится» на разность нового байта и старого. Что касается второго слова, то значение первого байта учтено в хеше то количество раз, каким является размер блока. Отняв от предыдущего значения слова произведение длины последовательности и значения первого байта, мы получим хеш, соответствующий последовательности без первого байта. Теперь добавим новое значение первого слова, отнимем единицу (иначе она будет учтена дважды) и получим новый хеш блока, смещенного на 1 байт вправо. При этом берем полученные значения по модулю.

inline uint32_t roll(int32_t adler, unsigned char oldbyte,
                     unsigned char newbyte, unsigned len){

    int32_t sum2 = (adler >> 16) & 0xffff;
    adler &= 0xffff;

    adler += newbyte - oldbyte;
    if(adler >= (int32_t) BASE)
        adler -= BASE;
    else if(adler < 0)
        adler += BASE;

    sum2 = (int32_t) (sum2 - len * oldbyte + adler - 1) %
           (int32_t) BASE;
    if(sum2 < 0){
        sum2 += BASE;

    return adler | (sum2 << 16);
}

Тут возможны отрицательные промежуточные значения, поэтому при операциях сравнения и модуля приходится приводить переменные и константы к числам со знаком. Для избежания переполнения на 32-разрядной архитектуре при перемножении длины блока на старый байт длина блока не должна превышать 8МБ. Функцию желательно объявить как inline, чтобы не расходовать дополнительное время на вызов функции и передачу аргументов.

Очевидно, у процесса оптимизации нет предела. И данная реализация тому не исключение. Например, можно изменить тип результата, возвращаемый функцией adler32 на структуру, содержащую два 32-разрядных слова, что может уменьшить количество ненужных операций по упаковке и распаковке хеш-суммы.


  1. На английском он называется rolling hash. Не могу сказать, насколько правомерен его перевод на русский как «кольцевой», потому как встречал его только один раз — на википедии, при этом без каких-либо ссылок на другие источники. 

Комментарии

RSS
Здесь можно Markdown