Обзор счётчиков Морриса

27 мая 2021

Мы рады представить вам заметку, написанную инженером Qrator Labs Дмитрием Камальдиновым. Если вы хотите быть частью нашей команды Core и заниматься подобными задачами - пишите нам на hr@qrator.net.

1 Введение

При реализации потоковых алгоритмов часто возникает задача подсчёта каких-то событий: приход пакета, установка соединения; при этом доступная память может стать узким местом: обычный \(n\)-битный счётчик позволяет учесть не более \(2^n - 1\) событий.
Одним из способов обработки большего диапазона значений, используя то же количество памяти, является вероятностный подсчёт. В этой статье будет предложен обзор известного алгоритма Морриса, а также некоторых его обобщений.

Другой способ уменьшить количество бит, необходимое для хранения значения счётчика, — использование распада. Об этом подходе мы рассказываем здесь, а также собираемся в ближайшее время опубликовать ещё одну заметку по теме.

Мы начнём с разбора простейшего алгоритма вероятностного подсчёта, выделим его недостатки (раздел 2). Затем (раздел 3) опишем алгоритм, впервые преложенный Робертом Моррисом в 1978 году, укажем его важнейшие свойства и приемущества. Для большинства нетривиальных формул и утверждений в тексте присутствуют наши доказательства — интересующийся читатель сможет найти их во вкладышах. В трёх последующих разделах мы изложим полезные расширения классического алгоритма: вы узнаете, что общего у счётчиков Морриса и экспоненциального распада, как можно уменьшить ошибку, пожертвовав максимальным значением, и как эффективно обрабатывать взвешенные события.

2 Вероятностный подсчёт, проблемы тривиального подхода

Основная идея вероятностного подсчёта — учёт очередного события с некоторой вероятностью. Рассмотрим сначала пример с постоянной вероятностью обновления:

\(\begin{equation*} C_{n+1} = \left\{\begin{array}{ll} C_n + 1 & \textit{с вероятностью $p = const$} \\ C_n & \textit{с вероятностью $1-p$.} \end{array}\right. \end{equation*}\)

Где через \(C_n\) обозначено значение счётчика после \(n\)-ой попытки обновления. Несложно понять, что \(C_n\) задаёт количество успехов среди \(n\) независимых испытаний Бернулли. Иначе говоря, \(C_n\) имеет биномиальное распределение:

\(\begin{align*} C_n \sim Bin(&n, p) \\ {\sf E} C_n = &np \\ {\sf D} C_n = &np(1-p). \end{align*}\)

В качестве оценки числа событий \(n\) естественно использовать \(\widehat{n}(C) := C/p\). Ясно, что такая оценка несмещённая: \({\sf E}\widehat{n}(C_n) = ({\sf E}C_n)/p = np/p = n\).

Описанный подход имеет несколько недостатков: во-первых, он позволяет учесть лишь в \(1/p = const\) раз больше событий по сравнению с простым инкрементальным счётчиком, чем заметно уступает счётчикам Морриса, как мы увидим далее. Во-вторых, относительная ошибка оценки велика при маленьких \(n\). Например, при \(n=1\) она вообще равна 100%. Оценить относительную ошибку можно с помощью коэффициента вариации:

\(\begin{align*} {\sf CV}\widehat{n}(C_n) = \frac{\sqrt{{\sf D}\widehat{n}(C_n)}}{{\sf E} \widehat{n}(C_n)} = \sqrt{\frac{1-p}{pn}}. \end{align*}\)

3 Счётчики Морриса

Счётчик Морриса обновляется с вероятностью, зависящей от текущего значения: первое обновление происходит с вероятностью 1, следующее с вероятностью 1/2, потом 1/4 и т.д.:

\(\begin{equation*} C_{n+1} = \left\{\begin{array}{ll} C_n + 1 & \textit{с вероятностью $2^{-C_n}$} \\ C_n & \textit{с вероятностью $1 - 2^{-C_n}$.} \end{array}\right. \end{equation*}\)

Как по значению счётчика оценить, сколько было событий? Обновление счётчика с \(x\) на \(x + 1\) происходит в среднем за \(2^x\) итераций, отсюда оценка числа событий \(\widehat{n}(C) := \sum_{i=0}^{C-1} 2^{i} = 2^C - 1\).

Важнейшими свойствами этой оценки являются

  • её несмещённость, то есть значение оценки после \(n\) вероятностных обновлений в среднем равно \(n\),

Доказательство

\(\begin{align*} {\sf E}\left(2^{C_n} | C_{n-1} = y\right) =& \underbrace{2^{y + 1} 2^{-y}}_{\textit{счётчик обновился}} + \underbrace{2^y (1 - 2^{-y})}_{\textit{значение не изменилось}} = 2^y + 1 \implies \\ {\sf E}\left(2^{C_n}|C_{n-1}\right) =& 2^{C_{n-1}} + 1 \implies {\sf E}2^{C_n} = {\sf E}2^{C_{n-1}} + 1 = {\sf E}2^{C_{n-2}} + 2 = \dots \end{align*}\)

В самом начале значение счётчика равно 0: \(C_0 = 0\), тогда \({\sf E}2^{C_0} = 2^{0} = 1\), а значит \({\sf E} 2^{C_n} = \dots = n + 1\). Итого \({\sf E}\widehat{n}(C_n) = {\sf E}2^{C_n} - 1 = n\).

  • а также независимость её относительной ошибки от \(n\).

Доказательство

Сначала посчитаем дисперсию \(\widehat{n}(C_n)\). Согласно известной формуле, \({\sf D}\widehat{n} = {\sf E}\widehat{n}^2 - ({\sf E}\widehat{n})^2\). Мы уже знаем, что \({\sf E}\widehat{n}(C_n) = n\). Осталось получить \({\sf E}\widehat{n}(C_n)^2\). Заметим, что

\( \begin{align*} {\sf E}\left(2^{C_n} - 1\right)^2 = {\sf E}2^{2C_n} - 2(n+1) + 1 = {\sf E}2^{2C_n} - 2n - 1. \end{align*}\)

Таким образом, остаётся узнать, чему равно \({\sf E}2^{2C_n}\).

\(\begin{align*} {\sf E}\left(2^{2C_n} | C_{n-1} = y\right) &= \underbrace{2^{2(y + 1)} 2^{-y}}_{\textit{счётчик обновился}} + \underbrace{2^{2y}(1 - 2^{-y})}_{\textit{значение не изменилось}} = \\ 3 \cdot 2^y +2^{2y} &\implies {\sf E}\left(2^{2C_n} | C_{n-1}\right) = 3 \cdot 2^{C_{n-1}} + 2^{2C_{n-1}} \implies \\ {\sf E} 2^{2C_n} = 3n &+ {\sf E}2^{2C_{n-1}} = 3n + 3(n-1) + {\sf E}2^{2C_{n-2}} = \dots = \sum_{i=1}^n 3i \end{align*}\)

Итого

\( \begin{equation*} {\sf D}\widehat{n}(C_n) = \frac{3n(n+1)}{2} - 2n - 1 - n^2 = \frac{n^2 - n - 2}{2} \end{equation*}\)

\(\begin{equation*} {} {\sf CV}\widehat{n}(C_n) = \frac{\sqrt{{\sf D}\widehat{n}(C_n)}}{{\sf E}\widehat{n}(C_n)} = \sqrt{\frac{n^2-n-2}{2n^2}} \sim \frac{1}{\sqrt{2}}. \end{equation*}\)

Таким образом, относительная ошибка оценки числа событий при использовании счётчика Морриса ограничена, примерно постоянна и почти не зависит от \(n\).
    
Оценить вероятность больших отклонений от среднего можно с помощью неравенства Чебышёва:

\(\begin{align*} {\sf P}(|\widehat{n}(C_n) - n| \ge \varepsilon n) \le \frac{{\sf D}\widehat{n}(C_n)}{\varepsilon^2n^2} \sim \frac{1}{2\varepsilon^2}. \end{align*}\)

  • отметим здесь также, что покрываемый диапазон количества событий при использовании \({\bf X}\) бит памяти будет \(2^{2^{\bf X}} - 1\). Иначе говоря, для учёта \(n\) событий достаточно \(\mathcal{O}(\log \log n)\) бит памяти!
Рис. 1: Относительная ошибка оценки числа событий при использовании биномиальных счётчиков и счётчиков Морриса*

*для построения графика \(sample\_size=100\) раз запускался процесс обновления счётчика, затем каждой точке \(n\) сопоставлялось среднее значение по этой выборке относительной ошибки после \(n\) обновлений

4 Взвешенные обновления

Счётчики Морриса можно использовать для подсчёта взвешенных событий: например, для суммирования размеров приходящих пакетов. Простейший способ — обновление счётчика <вес события> раз — приводит к линейной по весу сложности обновления.

Заметим, что в каждый момент времени мы знаем, как распределено количество неудач до ближайшего изменения счётчика, — это геометрическое рапределение:

\(\begin{align*} {\sf P}(n_{fail} = k) = (1 - 2^{-C})^k 2^{-C} \implies n_{fail} \sim Geom(2^{-C}). \end{align*}\)

Учитывая этот факт, мы можем ускорить алгоритм, сразу пропуская все подряд идущие неудачные попытки обновления:

5 Распад

Часто возникает потребность учитывать самые новые события с большим весом. Например, при поиске интенсивных ключей в потоке интереснее понять, какой ключ интенсивен прямо сейчас, а не имеет высокую среднюю интенсивность за счёт своего бурного прошлого. В статье мы рассказывали о распадных счётчиках, спроектированных специально для решения этой задачи. Другой предпосылкой для некоего распада является то, что несмотря на суперэкономное использование памяти, когда-то и счётчик Морриса переполнится. Оказывается, счётчики Морриса тоже могут распадаться, причём очень похожим на EMA способом, но гораздо эффективнее.

Идея очень проста: будем периодически вычитать \(1\) из счётчика. Напомним, что значение \(C\) оценивает количество событий как \(\sum_{i=0}^{C-1} 2^i = 2^C - 1 =: n_0\). Соответственно, после вычитания \(1\) оценка станет \(2^{C-1} - 1 = \frac{1}{2}n_0 - \frac{1}{2}\). Иначе говоря, вычитание \(1\) уменьшает оценку в \(\sim 2\) раза! Мы можем добиться распада ровно в \(2\) раза, обновив с вероятностью \(\frac{1}{2}\) счётчик сразу после вычитания \(1\):

6 Мантисса и экспонента

Рассмотрим следующее обобщение счётчика Морриса: вероятность обновления уменьшается в \(2\) раза не на каждое успешное обновление, а на каждые \({\bf X}\). Возможной реализацией такого подхода будет разделение битов счётчика на две части: экспоненту, отвечающую за вероятность, и мантиссу, считающую количество успешных обновлений при текущей вероятности (чем-то похоже на представление чисел с плавающей точкой).

Рис. 2: разбиение битов счётчика на мантиссу (\({\bf M} = 5\) бит) и экспоненту (\({\bf E} = 3\) бит)

Введём обозначения для мантиссы и экспоненты:

\(\begin{align*} {\rm e}(C) &= C \gg {\bf M} \\ {\rm m}(C) &= C \& (2^{\bf M} - 1). \end{align*}\)

Теперь правило обновления и оценка числа событий записываются так:

\(\begin{align*} C_{n+1} &= \left\{\begin{array}{ll} C_n + 1, & \textit{с вероятностью $2^{-{\rm e}(C)}$} \\ C_n, & \textit{с вероятностью $1 - 2^{-{\rm e}(C)}$} \end{array}\right. \\ \widehat{n}(C) &= (2^{{\rm e}(C)} - 1)2^{\bf M} + 2^{{\rm e}(C)}{\rm m}(C). \end{align*}\)

Например, на рисунке 2 значение счётчика \(C = 89\), мантисса и экспонента равны \({\rm m} = 25\) и \({\rm e} = 2\) соответственно. А значит число событий оценивается как \(\widehat{n} = (2^2 - 1) \times 2^5 + 2^2 \times 25 = 196\).

При таком разбиении биты экспоненты будут отвечать за покрываемый диапазон количества событий, а биты мантиссы за точность оценки (далее мы поясним подробнее, что это значит). Наилучшее распределение битов счётчика зависит от задачи; мы предлагаем отдавать как можно больше мантиссе, но чтобы экспоненциальных битов хватало для покрытия интересующего диапазона значений.

Описанный вариант счётчиков Морриса обладает следующими свойствами:

  • покрываемый диапазон значений равен

\(\begin{align*} {\bf N}_{max} = 2^{2^{\bf E} + {\bf M}} - \left(2^{2^{\bf E} - 1} + 2^{\bf M}\right); \end{align*}\)

Доказательство

Покрываемый диапазон получить несложно, подставив в формулу для \(\widehat{n}\) максимальные возможные значения \({\rm m} = 2^{\bf M} - 1\) и \({\rm e} = 2^{\bf E} - 1\):

\(\begin{align*} {\bf N}_{max} = \left(2^{2^{\bf E} - 1} - 1\right)2^{\bf M} + 2^{2^{\bf E} - 1}\left(2^{\bf M} - 1\right) = 2^{2^{\bf E} + {\bf M}} - \left(2^{2^{\bf E} - 1} + 2^{\bf M}\right) \end{align*}\)

  • новая оценка \(\widehat{n}\) также несмещённая: \({\sf E}\widehat{n}(C_n) = n\);

Доказательство

Аналогично доказательству утверждения для обычных счётчиков Морриса посчитаем условное мат.~ожидание \({\sf E}(C_n | C_{n - 1} = y)\). Причём сделаем это отдельно для \(y \equiv -1 \pmod{2^{\bf M}}\) (вероятность обновления изменится в случае успеха) и \(y \not\equiv -1 \pmod{2^{\bf M}}\).

\(\begin{align*} y \not\equiv -1 \pmod{2^{\bf M}} &\implies {\rm e}(y + 1) = {\rm e}(y), \quad {\rm m}(y + 1) = {\rm m}(y) + 1 \\ &\implies \widehat{n}(y + 1) = \widehat{n}(y) + 2^{{\rm e}(y)} \\ y \equiv -1 \pmod{2^{\bf M}} & \implies {\rm e}(y + 1) = {\rm e}(y) + 1,\\ &\phantom{\implies} \phantom{Z} {\rm m}(y) = 2^{\bf M} - 1, \quad {\rm m}(y + 1) = 0 \implies \\ \widehat{n}(y + 1) = \widehat{n}(y) &- 2^{{\rm e}(y)}\left(2^{\bf M} - 1\right) + 2^{{\rm e}(y) + {\bf M}} = \widehat{n}(y) + 2^{{\rm e}(y)} \end{align*}\)

В обоих случаях

\(\begin{align*} {\sf E}(2^{C_n} | C_{n-1} = y) &= \widehat{n}(y)\left(1 - 2^{-{\rm e}(y)}\right) + \left(\widehat{n}(y) + 2^{{\rm e}(y)}\right)2^{-{\rm e}(y)} = \widehat{n}(y) + 1. \end{align*}\)

  • коэффициент вариации (и относительная ошибка) становятся ещё меньше! Мы не знаем его точное значение, но имеется такая оценка (сравните её с аналогичной для классического счётчика Морриса):

\(\begin{align*} {\sf CV}\widehat{n}(C_n) \le 2^{-\frac{{\bf M} + 1}{2}}. \end{align*}.\)

Рис. 3: Чтение и обновление счётчика Морриса с ненулевой мантиссой

Алгоритмы взвешенных обновлений и распада тоже немного изменятся.

Аналогично подходу, описанному в разделе 4, чтобы эффективно выполнить взвешенное обновление, нужно понять, через сколько попыток обновления вероятность изменится. При отсутствии мантиссы вероятность меняется после каждого успешного обновления, теперь же после каждых \(2^{\bf M}\). А значит нам нужно распределение количества испытаний Бернулли (искомое число событий), среди которых ровно \(2^{\bf M}\) успешных. Такое распределение называется отрицательным биномиальным.

Замечание. Существует несколько определений отрицательного биномиального распределения. Будем придерживаться написанного в русскоязычной википедии, где сказано, что это распределение задаёт не общее число событий, а число неудач. Тогда чтобы получить число событий, нужно к случайной величине добавить количество успехов:

\(\begin{align*} n \sim \underbrace{ NegativeBinomial\left(2^{-{\rm e}(C)}, 2^{\bf M} - {\rm m}(C)\right)}_{\textit{неудач}} + \underbrace{\left(2^{\bf M} - {\rm m}(C)\right)}_{\textit{успехов}}. \end{align*}\)

Чтобы не генерировать отрицательное биномиальное распределение, можно загрубить алгоритм, всегда выбирая среднее этого распределения:

Чтобы выполнить распад, будем вычитать \(1\) из экспоненциальной части (иначе говоря, \(2^{\bf M}\) из значения счётчика). Тогда

\(\begin{align*} \widehat{n}(C - 2^{\bf M}) = (2^{{\rm e}(C) - 1} - 1)2^{\bf M} + 2^{{\rm e}(C) - 1}{\rm m}(C) = 1/2\widehat{n}(C) - 2^{{\bf M} - 1}. \end{align*}\)

Заметим, что при \({\bf M} > 0\) (а именно этот случай изучается в этом разделе) \(2^{{\bf M} - 1} \in \mathbb{N}\). Поэтому исправить оценку можно, вероятностно увеличив счётчик на \(2^{{\bf M} - 1}\):

7 Заключение

Счётчики Морриса — это очень круто! Пользуйтесь!

Последний раздел не содержит полезных на практике результатов, но...

8 Обобщённые вероятностные счётчики

В общем виде правило обновления счётчика определяется функцией вероятности \(F : C \mapsto [0, 1]\):

\(\begin{equation*} C_{n+1} = \left\{\begin{array}{ll} C_n + 1 & \textit{с вероятностью $F(C_n)$} \\ C_n & \textit{с вероятностью $1 - F(C_n)$.} \end{array}\right. \end{equation*}\)

В случае счётчиков Морриса \(F(C_n) = 2^{-C_n}\). При изучении вероятностных счётчиков мы попробовали взять линейную функцию вероятности: \(F(C_n) = 1 - \frac{C_n}{C_{max}}\) (\(C_{max}\) — некоторая константа, максимальное значение счётчика). Это привело к некоторым интересным результатам, которые будут кратко изложены в этом разделе.

Прежде чем представить эти результаты, напомним о двух математических сущностях.
Полный однородный симметрический многочлен степени \(n\) от \(m\) переменных \(x_1, \dots, x_m\) — это сумма всех возможных одночленов степени \(n\) от этих переменных с коэффициентами \(1\):

\(\begin{align*} \mathcal{H}_n(x_1, \dots x_m) = \sum_{\begin{array}{cc} & 0 \le i_j \le n \\ & i_1 + \dots + i_m = n \end{array}} x_1^{i_1} \cdots x_m^{i_m}. \end{align*}\)

Другая конструкция, которая нам понадобится, — число Стирлинга второго рода. Один из способов определить это число — комбинаторный: число Стирлинга второго рода \(\genfrac\{\}{0pt}{}{n}{m}\) — количество разбиений \(n\)-элементного множества на \(m\) непустых подмножеств. Числа Стирлинга связаны с полными симметрическим многочленами:

\(\begin{align*} \genfrac\{\}{0pt}{}{n+m}{m} = \mathcal{H}_m(1, 2, \dots, m). \end{align*}\)

С помощью \(\mathcal{H}\) можно записать в общем виде вероятность того, что после \(n\) попыток обновления значение счётчика равно \(m\):

\(\begin{align*} {\sf P}(C_n = m) = \left[\prod_{k=0}^{m-1} F(k) \right] \mathcal{H}_{n-m}(1-F(1), \dots 1-F(m)). \end{align*}\)

Эту формулу легко доказать по индукции, но также она несложно следует из простых комбинаторных рассуждений: фактически в ней записано, что для того, чтобы получить значение \(m\), нужны \(m\) успешных обновлений: с \(0\) до \(1\), с \(1\) до \(2\) и т.д., — за это отвечает множитель \(\prod_{k=0}^{m-1} F(k)\), и \(n - m\) неудачных, которые разбиваются на \(m\) групп: \(i_1\) неудачных обновлений с \(1\) до \(2\), \(i_2\) с \(2\) до \(3\) и т.д., что выражено \(\mathcal{H}_{n-m}\).

Наконец, при использовании линейной функции вероятности \(F(C) = 1 - \frac{C}{C_{max}}\) вероятность принимает вид

\(\begin{equation*} {\sf P}(C_n = m) = \genfrac\{\}{0pt}{}{n}{m}\frac{C_{max}!}{(C_{max} - m)!C_{max}^n}. \end{equation*}\)

Это и есть анонсированный выше интересный результат. Мы заметили число Стирлинга в значении вероятности достаточно случайно и до сих пор не знаем, можно ли обосновать его присутствие комбинаторынми рассуждениями (опираясь, на его комбинаторное определение). Другой вопрос — можно красиво записать аналогичную вероятность для счётчиков Морриса.

Математическое ожидание и дисперсия \(C_n\) (доказательства опускаем, но их несложно получить, используя формулу для \({\sf P}\) и свойства чисел Стирлинга).

\(\begin{align*} {\sf E}C_n &= C_{max}\left(1 - \left(1 - \frac{1}{C_{max}}\right)^n\right) \\ {\sf D}C_n &= C_{max}^2\left(\frac{1}{C_{max}}\left(1 - \frac{1}{C_{max}}\right)^n + \left(1 - \frac{1}{C_{max}}\right)\left(1 - \frac{2}{C_{max}}\right)^n - \left(1 - \frac{1}{C_{max}}\right)^{2n}\right). \end{align*}\)

Оценить число событий можно по методу моментов:

\(\begin{equation*} \widehat{n}_{MM} := \frac{\ln \left(1 - \frac{C_n}{C_{max}}\right)}{\ln \left(1 - \frac{1}{C_{max}}\right)} \end{equation*}\)

или аналогично счётчикам Морриса, учитывая, что \(1 + 1/2 + \dots + 1/n \sim \ln n\):

\(\begin{equation*} \widehat{n} := C_{max}\ln \left(\frac{1}{1 - \frac{C_n}{C_{max}}}\right). \end{equation*}\)

Список литературы

[1] Morris, R.Counting large numbers of events in small registersCommunications of the ACM, 1978 21(10), 840-842.

[2] http://gregorygundersen.com/blog/2019/11/11/morris-algorithm/ − хороший обзор счётчиков Морриса на английском языке

[3] https://habr.com/ru/company/qrator/blog/334354/ − наша статья про EMA-счётчики

[4] https://habr.com/ru/post/208268/ − сторонний материал про счётчики Морриса

Ознакомьтесь со всеми функциями
в 7-дневной пробной версии

Наши специалисты готовы оказать вам поддержку

Получите ваш отчет

Подпишитесь на ежемесячный обзор самых популярных новостей Интернета от CURATOR!

Будьте в курсе наших новостей и продуктов!

Подпишитесь на ежемесячный обзор самых популярных идей Интернета от CURATOR!

E-mail *

Отправляя свой адрес электронной почты, вы соглашаетесь с политикой конфиденциальности

Получите ваш отчет
Имя *
Email *
Должность *
Компания *

Отправляя свои данные, вы соглашаетесь с политикой конфиденциальности

Спасибо что остаетесь с нами!

Если документ не загружается, нажмите кнопку «Загрузить». Помогите нам лучше понять рынок и подготовить лучшую аналитику, пройдите опрос.

Загрузить
Пройдите опрос

Поделитесь своим опытом и ожиданиями в отношении защиты от DDoS-атак. Ваши ответы помогут нам предложить решения, соответствующие вашим потребностям в области кибербезопасности

Расскажите нам о инфраструктуре вашей компании и критически важных системах. Это поможет нам понять масштаб необходимой защиты

Помогите нам понять, как в вашей компании принимаются решения. Эта информация позволит предложить наиболее актуальные решения

Расскажите, что влияет на ваш выбор в вопросах защиты от DDoS-атак. Ваше мнение поможет нам сосредоточиться на самом важном для вас

1}/4.
Что для вас самое важное при выборе Anti-DDoS решения? (можно выбрать несколько)
Кто в вашей компании принимает решения о выборе решений кибербезопасности?
Какой средний объем интернет-трафика вашей компании?
Mb
Вы сталкивались с DDoS-атаками ранее?
Какие ключевые риски вы хотите минимизировать с помощью защиты от DDoS?
Когда вы планируете рассматривать решение для защиты от DDoS?
Какие системы критически важны для вашего бизнеса защитить? (можно выбрать несколько)
Есть ли у вашей компании решения для защиты от DDoS-атак?
Какой основной тип деятельности вашей компании?
Какой уровень защиты от DDoS вы считаете достаточным?
Размер вашей команды
Спасибо за прохождение опроса!

Ваше участие поможет нам улучшить аналитику рынка.

Ваша подписка успешно активирована
На главную