Использование CRC в качестве дайджеста для обнаружения дубликатов среди файлов

Основное использование CRC и подобных вычислений (таких как Флетчер и Адлер), по-видимому, предназначено для обнаружения ошибок передачи. Таким образом, большинство исследований, которые я видел, похоже, посвящены проблеме вероятности обнаружения небольших различий между двумя наборами данных. Мои потребности немного другие.

Ниже приводится очень приблизительное описание проблемы. Детали намного сложнее, но приведенное ниже описание иллюстрирует функции, которые я ищу. Этот небольшой отказ от ответственности предназначен для защиты от таких ответов, как «Почему вы решаете свою проблему таким образом, если вам легче решить ее другим способом, который я предлагаю?» - Мне нужно решить мою проблему таким образом по множеству причин, которые не имеют отношения к этому вопросу или сообщению, поэтому, пожалуйста, не публикуйте такие ответы.

Я имею дело с коллекциями наборов данных (размером ~ 1 МБ) в распределенной сети. Вычисления выполняются с этими наборами данных, и скорость / производительность имеют решающее значение. Мне нужен механизм, позволяющий избежать повторной передачи наборов данных. То есть мне нужен способ сгенерировать уникальный идентификатор (UID) для каждого набора данных заданного размера. (Затем я передаю размер набора данных и UID с одной машины на другую, и принимающей машине нужно только запросить передачу данных, если у нее их еще нет локально, на основе UID.)

Это похоже на разницу между использованием CRC для проверки изменений файла и использованием CRC в качестве дайджеста для обнаружения дубликатов среди файлов. Я не видел никаких обсуждений последнего использования.

Меня не волнуют вопросы подделки, т.е. мне не нужно хеширование криптографической стойкости.

В настоящее время я использую простую 32-битную CRC сериализованных данных, и до сих пор она мне хорошо служила. Однако я хотел бы знать, может ли кто-нибудь порекомендовать, какой 32-битный алгоритм CRC (т.е. какой полином?) Лучше всего подходит для минимизации вероятности коллизий в этой ситуации?

Другой вопрос, который у меня есть, немного более тонкий. В моей текущей реализации я игнорирую структуру своего набора данных и фактически просто проверяю CRC сериализованной строки, представляющей мои данные. Однако по разным причинам я хочу изменить свою методологию CRC следующим образом. Предположим, мой набор данных верхнего уровня представляет собой набор необработанных данных и нескольких подчиненных наборов данных. Моя текущая схема по существу объединяет необработанные данные и все подчиненные наборы данных, а затем результат CRC. Однако в большинстве случаев у меня уже есть CRC подчиненных наборов данных, и я бы предпочел построить свой UID набора данных верхнего уровня, объединив необработанные данные с CRC подчиненные наборы данных, а затем CRC этой конструкции. Вопрос в том, как использование этой методики влияет на вероятность столкновений?

Чтобы выразить это на языке, который позволит мне обсуждать мои мысли, я определю некоторые обозначения. Назовите мой набор данных верхнего уровня T и предположим, что он состоит из набора исходных данных R и подчиненных наборов данных Si, i=1..n. Я могу написать это как T = (R, S1, S2, ..., Sn). Если & представляет собой объединение наборов данных, мою исходную схему можно представить как:

UID_1(T) = CRC(R & S1 & S2 & ... & Sn)

и мою новую схему можно представить как

UID_2(T) = CRC(R & CRC(S1) & CRC(S2) & ... & CRC(Sn))

Тогда мои вопросы: (1) если T и T' очень разные, какой алгоритм CRC минимизирует prob( UID_1(T)=UID_1(T') ), а какой алгоритм CRC минимизирует prob( UID_2(T)=UID_2(T') ), и как сравнить эти две вероятности?

Мои (наивные и неосведомленные) мысли по этому поводу таковы. Предположим, что различия между T и T' находятся только в одном подчиненном наборе данных, WLOG сообщает S1!=S1'. Если случится, что CRC(S1)=CRC(S1'), то явно у нас будет UID_2(T)=UID_2(T'). С другой стороны, если CRC(S1)!=CRC(S1'), тогда разница между R & CRC(S1) & CRC(S2) & ... & CRC(Sn) и R & CRC(S1') & CRC(S2) & ... & CRC(Sn) небольшая разница только в 4 байта, поэтому способность UID_2 обнаруживать различия фактически такая же, как способность CRC обнаруживать ошибки передачи, то есть его способность обнаруживать ошибки только в нескольких битах, которые не разделены на большие расстояния. Поскольку это то, для чего предназначены CRC, я бы подумал, что UID_2 довольно безопасен, если CRC, который я использую, хорош для обнаружения ошибок передачи. Выражаясь в наших обозначениях,

prob( UID_2(T)=UID_2(T') ) = prob(CRC(S1)=CRC(S1')) + (1-prob(CRC(S1)=CRC(S1'))) * probability of CRC not detecting error a few bits.

Назовем вероятность того, что CRC не обнаружит ошибку в несколько битов P, и вероятность того, что она не обнаружит больших различий в наборе данных большого размера Q. Сказанное можно приблизительно записать как

prob( UID_2(T)=UID_2(T') ) ~ Q + (1-Q)*P

Теперь я немного изменю свой UID следующим образом. Для «фундаментальной» части данных, то есть набора данных T=(R), где R - это просто двойное, целое число, символ, логическое значение и т. Д., Определите UID_3(T)=(R). Затем для набора данных T, состоящего из вектора подчиненных наборов данных T = (S1, S2, ..., Sn), определите

UID_3(T) = CRC(ID_3(S1) & ID_3(S2) & ... & ID_3(Sn))

Предположим, что в конкретном наборе данных T есть подчиненные наборы данных, вложенные m на глубину, тогда, в некотором неопределенном смысле, я мог бы подумать, что

prob( UID_3(T)=UID_3(T') ) ~ 1 - (1-Q)(1-P)^m

Учитывая, что эти вероятности в любом случае малы, это можно аппроксимировать как

 1 - (1-Q)(1-P)^m = Q + (1-Q)*P*m + (1-Q)*P*P*m*(m-1)/2 + ... ~ Q + m*P

Итак, если я знаю свой максимальный уровень вложенности m и знаю P и Q для различных CRC, я хочу выбрать CRC, который дает мне минимальное значение для Q + m*P. Если, как я подозреваю, может иметь место P~Q, вышеизложенное упрощается до этого. Моя вероятность ошибки для UID_1 равна P. Моя вероятность ошибки для UID_3 равна (m+1)P, где m - мой максимальный уровень вложенности (рекурсии).

Все это кажется разумным?


person David I. McIntosh    schedule 23.03.2013    source источник


Ответы (3)


Мне нужен механизм, позволяющий избежать повторной передачи наборов данных.

rsync уже решил эту проблему, используя в целом описанный вами подход.

Однако я хотел бы знать, может ли кто-нибудь порекомендовать, какой 32-битный алгоритм CRC (т.е. какой полином?) Лучше всего подходит для минимизации вероятности коллизий в этой ситуации?

Вы не увидите большой разницы между хорошо подобранными полиномами CRC. Скорость может быть для вас более важной, и в этом случае вы можете использовать аппаратный CRC, например crc32 инструкция по современным процессорам Intel. Этот использует полином CRC-32C (Кастаньоли) . Вы можете сделать это действительно быстро, используя все три арифметических модуля на одном ядре параллельно, вычисляя CRC на трех буферах в одном цикле, а затем комбинируя их. См. Ниже, как объединить CRC.

Однако в большинстве случаев у меня уже есть CRC подчиненных наборов данных, и я бы предпочел построить свой UID набора данных верхнего уровня, объединив необработанные данные с CRC подчиненных наборов данных, а затем CRC этой конструкции .

Или вы можете быстро вычислить CRC всего набора, как если бы вы выполнили CRC всего, но с использованием уже вычисленных CRC частей. Посмотрите на crc32_combine() в zlib. Это было бы лучше, чем брать CRC для кучи CRC. Комбинируя, вы сохраняете все математические качества алгоритма CRC.

person Mark Adler    schedule 23.03.2013
comment
Отличная информация! Не знал ни о Intel crc32, ни о том, что можно реализовать crc32_combine! crc32_combine делает мои бессвязные разговоры неуместными. ржу не могу. В моем случае распараллеливание не помогает - я либо вычисляю CRC в процессе сериализации, которая не может быть распараллелена, либо у меня уже есть CRC компонентов, и тогда crc32_combine отлично справляется с задачей. Спасибо, Марк. - person David I. McIntosh; 24.03.2013
comment
Просто из любопытства, раз уж вы тот, кто мог бы знать, были ли мои рассуждения хотя бы приблизительно правильными? (хотя и совершенно бесполезно с учетом возможностей crc32_combine). - person David I. McIntosh; 24.03.2013
comment
Не думаю, что вы понимаете описанное мною распараллеливание. Это применяется в одиночном ядре к единственному вычислению CRC, как вы делаете сейчас для каждого из ваших компонентов. Распараллеливание делает CRC этого компонента в три раза быстрее. - person Mark Adler; 24.03.2013
comment
Что касается бессмысленности, я в основном перестал читать после того, как увидел, что вы не знали о способности комбинировать CRC. Общий комментарий заключается в том, что если T и T ' сильно различаются, как вы утверждаете, то детали расчета чека не имеют большого значения, если информация в сообщение хорошо смешано с контрольным значением. Что это для любого стандартного CRC. В случае большого количества ошибок вероятность ложного срабатывания зависит только от количества бит в контрольном значении. - person Mark Adler; 24.03.2013
comment
Быстрый вопрос: я вижу, что процесс объединения двух CRC включает умножение матриц (log_2 N). У вас есть приблизительное представление о минимальной длине данных, при которой использование crc32_combine более эффективно, чем просто запуск алгоритма CRC? - person David I. McIntosh; 24.03.2013
comment
Наконец, я думаю, что понял описываемое вами распараллеливание, и если бы я вычислял CRC изолированно, это помогло бы. Дело в том, что для компонента я не могу вычислить CRC, пока он не сериализован, но я могу вычислить CRC _ while_serializing его, при этом стоимость фактического вычисления CRC незначительна по сравнению со стоимостью сериализации. А процесс сериализации нельзя ускорить с помощью распараллеливания. (Другими словами, мой расчет CRC встроен в процесс сериализации.) - person David I. McIntosh; 24.03.2013
comment
Что касается распараллеливания, у вас должен быть некоторый размер буфера, который вы используете для данных в памяти, для которых вы вычисляете CRC. Вы можете просто сделать этот размер буфера кратным трем, например 768 и выполните три CRC для трех 256-байтовых частей. Вы можете пойти еще меньше и по-прежнему получать выгоду. - person Mark Adler; 24.03.2013
comment
О, я должен упомянуть, что если длина второй части фиксирована, то есть вы всегда вызываете crc32_combine() с тем же len2, тогда вы можете предварительно рассчитать оператор, который будет применяться, и тогда комбинация практически не займет времени. - person Mark Adler; 24.03.2013
comment
позвольте нам продолжить обсуждение в чате - person David I. McIntosh; 24.03.2013

Ответ Марка Адлера был удачным. Если бы я снял шляпу программиста и надел шляпу математика, кое-что из этого было бы очевидным. У него не было времени объяснять математику, так что я здесь для тех, кому интересно.

Процесс вычисления CRC - это, по сути, процесс полиномиального деления. Многочлены имеют коэффициенты по модулю 2, то есть коэффициент каждого члена равен 0 или 1, следовательно, многочлен степени N может быть представлен N-битным числом, каждый бит является коэффициентом члена (и процесс выполнения полиномиальное деление означает выполнение целого ряда операций XOR и сдвига). При проверке CRC блока данных мы рассматриваем «данные» как один большой многочлен, то есть длинную строку битов, каждый бит представляет коэффициент члена в многочлене. Назовем наш полином блока данных A. Для каждой «версии» CRC был выбран полином для CRC, который мы назовем P. Для 32-битных CRC P является полиномом. со степенью 32, поэтому в нем 33 члена и 33 коэффициента. Поскольку верхний коэффициент всегда равен 1, он является неявным, и мы можем представить многочлен 32-й степени 32-битным целым числом. (С вычислительной точки зрения это на самом деле довольно удобно.) Процесс вычисления CRC для блока данных A - это процесс нахождения остатка от деления A на P. То есть A всегда можно записать

A = Q * P + R

где R - многочлен степени меньше степени P, то есть R имеет степень 31 или меньше, поэтому он может быть представлен 32-битным целым числом. R - это, по сути, CRC. (Небольшое примечание: обычно к A добавляется 0xFFFFFFFF, но здесь это неважно.) Теперь, если мы объединим два блока данных A и B, «полином», соответствующий объединению двух блоков, будет полиномом для A, сдвинутым слева "на количество битов в B плюс B. Другими словами, многочлен для A&B равен A * S + B, где S - это многочлен, соответствующий единице, за которой следует N нулей, где N - количество биты в B. (т.е. S = x ** N). Тогда что мы можем сказать о CRC для A&B? Предположим, мы знаем A = Q * P + R и B = Q '* P + R', т.е. R - это CRC для A, а R '- это CRC для B. Предположим, мы также знаем, что S = q * P + r. потом

A * S + B = (Q*P+R)*(q*P+r) + (Q'*P+R')
          = Q*(q*P+r)*P + R*q*P + R*r + Q'*P + R'
          = (Q*S + R*q + Q') * P    + R*r + R'

Таким образом, чтобы найти остаток, когда A * S + B делится на P, нам нужно только найти остаток, когда R * r + R 'делится на P. Таким образом, чтобы вычислить CRC конкатенации двух потоков данных A и B , нам нужно знать только отдельные CRC потоков данных, то есть R и R ', и длину N конечного потока данных B (чтобы мы могли вычислить r). Это также содержание одного из комментариев Marks other: если длины конечных потоков данных B ограничены несколькими значениями, мы можем предварительно вычислить r для каждой из этих длин, что делает комбинацию двух CRC довольно тривиальной. (Для произвольной длины N вычисление r нетривиально, но это намного быстрее (log_2 N), чем повторное деление по всей длине B.)

Примечание: приведенное выше не является точным описанием CRC. Происходят некоторые сдвиги. Чтобы быть точным, если L - многочлен, представленный 0xFFFFFFFF, то есть L = x * 31 + x * 30 + ... + x + 1, а S_n - это «сдвиг влево на n битов» полином, то есть S_n = x ** n, тогда CRC блока данных с полиномом A из N битов является остатком, когда (L * S_N + A) * S_32 делится на P, то есть когда (L&A) * S_32 равно делится на P, где & - оператор «конкатенации».

Кроме того, я не согласен с одним из комментариев Марка, но он может поправить меня, если я ошибаюсь. Если мы уже знаем R и R ', сравнение времени для вычисления CRC A&B с использованием вышеупомянутой методологии по сравнению с вычислением его прямым способом не зависит от отношения len (A) к len (B) - к вычислить его "прямым" способом, действительно не нужно повторно вычислять CRC для всего связанного набора данных. Используя наши обозначения выше, нужно только вычислить CRC R * S + B. То есть вместо того, чтобы предварительно отложить 0xFFFFFFFF до B и вычислить его CRC, мы добавляем R к B и вычисляем его CRC. Таким образом, это сравнение времени для повторного вычисления CRC B со временем для вычисления r (с последующим делением R * r + R 'на P, что, вероятно, тривиально и несущественно по времени).

person David I. McIntosh    schedule 25.03.2013
comment
Вы правы - мой комментарий предполагал повторное вычисление CRC всего этого, но вам не нужно, если у вас есть CRC первого фрагмента. Я удалил этот комментарий. - person Mark Adler; 08.04.2013

ответ Марка Адлера касается технического вопроса, поэтому я не буду этим заниматься здесь. Здесь я собираюсь указать на серьезную потенциальную ошибку в алгоритме синхронизации, предложенном в вопросе ОП, и предложить небольшое улучшение.

Контрольные суммы и хэши предоставляют единое значение подписи для некоторых данных. Однако, поскольку они имеют конечную длину, количество возможных уникальных значений контрольной суммы / хэша всегда меньше возможных комбинаций необработанных данных, если данные длиннее. Например, 4-байтовый CRC может принимать только 4 294 967 296 уникальных значений, в то время как даже 5-байтовое значение, которое может быть данными, может принимать в 8 раз больше значений. Это означает, что для любых данных, длина которых превышает длину самой контрольной суммы, всегда существует одна или несколько комбинаций байтов с точно такой же подписью.

При использовании для проверки целостности предполагается, что вероятность того, что несколько другой поток данных приведет к одной и той же подписи, мала, поэтому мы можем предположить, что данные одинаковы, если подпись одинакова. Важно отметить, что мы начинаем с некоторых данных d и проверяем, что с учетом контрольной суммы c, вычисленной с использованием функции контрольной суммы, f, что f(d) == c.

Однако в алгоритме OP различное использование вносит незаметное пагубное снижение уверенности. В алгоритме OP сервер A будет начинать с необработанных данных [d1A,d2A,d3A,d4A] и генерировать набор контрольных сумм [c1,c2,c3,c4] (где dnA - n-й элемент данных на сервере A). Затем сервер B получит этот список контрольных сумм и проверит свой собственный список контрольных сумм, чтобы определить, отсутствуют ли какие-либо. Скажем, на сервере B есть список [c1,c2,c3,c5]. Тогда должно произойти то, что он запросит d4 с сервера A, и в идеальном случае синхронизация сработала правильно.

Если мы вспомним о возможности коллизий и о том, что для их создания не всегда требуется столько данных (например, CRC("plumless") == CRC("buckeroo")), то мы быстро поймем, что лучшая гарантия, которую предоставляет наша схема, - это то, что сервер B определенно не имеет d4A но он не может гарантировать, что у него есть [d1A,d2A,d3A]. Это потому, что возможно, что f(d1A) = c1 и f(d1B) = c1, хотя d1A и d1B различны, и мы хотели бы, чтобы на обоих серверах были оба. В этой схеме ни один из серверов никогда не может знать о существовании как d1A, так и d1B. Мы можем использовать все больше и больше контрольных сумм и хэшей, устойчивых к коллизиям, но эта схема никогда не может гарантировать полную синхронизацию. Это становится более важным, чем большее количество файлов должна отслеживать сеть. Я бы рекомендовал использовать криптографический хеш, например SHA1, для которого не было обнаружено коллизий.

Возможное снижение этого риска - введение избыточных хэшей. Один из способов сделать это - использовать совершенно другой алгоритм, поскольку, хотя это возможно crc32(d1) == crc32(d2), менее вероятно, что adler32(d1) == adler32(d2) одновременно. В этой статье говорится, что вы не получите всего таким образом. Если использовать обозначение OP, то также менее вероятно, что crc32('a' & d1) == crc32('a' & d2) и crc32('b' & d1) == crc32('b' & d2) одновременно истинны, поэтому вы можете "добавить" к комбинациям с меньшей вероятностью столкновений. Однако я думаю, что вы можете просто использовать хэш-функцию, устойчивую к столкновениям, такую ​​как SHA512, которая на практике, вероятно, не окажет большого влияния на вашу производительность.

person jeteon    schedule 20.08.2015
comment
Вы правы во всем, что говорите. Но две вещи об этой конкретной проблеме делают ваши комментарии не совсем применимыми. (1) Используя ваши обозначения, некоторые особенности наборов данных dnA и dnB (для фиксированного n) делают крайне маловероятным, что f (dnA) = f (dnB), если dnA = dnB. В частности, dnA и dnB будут различаться только ограниченным образом, так что их CRC практически гарантированно будут разными. Это особенность нашей проблемы - мы не создаем общие инструменты для какого-то продукта. И (2) в моем первоначальном посте я недостаточно подчеркнул важность производительности. - person David I. McIntosh; 22.08.2015
comment
В частности, обратите внимание, что с помощью описанной выше работы и некоторых других упрощений, учитывая CRC (A) и CRC (B), можно вычислить CRC (A&B) практически мгновенно, особенно на современных процессорах Intel с инструкцией CRC (которая также позволяет вычислять CRC (X) очень быстро). Однако ваши комментарии будут служить предупреждением для всех, кто попытается использовать этот подход для общей синхронизации. - person David I. McIntosh; 22.08.2015
comment
Наверное, я этого не говорил, но озабоченность была больше, чем его возможное f(dnA) = f(dmB) (возможно, разные индексы). Однако, поскольку вы говорите, что ваша проблема структурирована так, что по построению этого не может произойти, я согласен с тем, что этот ответ является более общим, чем то, что вы искали. Было интересно обнаружить, что для тестов общих реализаций, с которыми я работал, разница в производительности между MD5 и CRC32 может быть меньше 2 раз. - person jeteon; 22.08.2015
comment
Как реализован CRC32? В частности, использует ли он машинную инструкцию Intel и реализует ли предложенную Марком схему параллельного объединения и объединения? Я прошу из интереса, не оспаривать, потому что, опять же, в наших обстоятельствах мы можем вычислить CRC32 почти бесплатно при сериализации. Это могло быть верно и для MD5 - я недостаточно знаю об этом. Кроме того, если бы мы сравнивали f (dnA) с f (dmB), действительно были бы очень серьезные проблемы с деградацией, на несколько порядков больше вероятность коллизий. - person David I. McIntosh; 23.08.2015
comment
CRC32 в тестах, как правило, был реализован на C, они не использовали машинные инструкции. По данным strchr.com/crc32_popcnt, инструкция CRC32 кажется более чем в 5 раз быстрее. Хеш MD5 представляет состояние, достигнутое в конце хеширования. Насколько я знаю, вы должны просто хешировать файлы последовательно, чтобы получить объединенный хэш (каждый раз начиная с последнего значения хеша). Однако я не думаю, что есть способ получить общую сумму MD5 с учетом сумм компонентов. Однако вы можете просто хешировать составные хэши. - person jeteon; 24.08.2015