Быстрая альтернатива редукции по модулю (2016)

Быстраяальтернативаредукциипомодулю2016

Предположим, вы хотите случайным образом выбрать целое число из набора из N элементов. Ваш компьютер имеет функции для генерации случайных 039 – битовые целые числа, как преобразовать такие числа в индексы не больше N ? Предположим, у вас есть хеш-таблица с емкостью N . Опять же, вам нужно преобразовать свои хеш-значения (обычно 46 – бит или 69 – битовые целые числа) до индекса не больше чем N . Программисты часто решают эту проблему, убедившись, что N является степенью двойки, но это не всегда идеально.

Нам нужна карта что как можно более справедливо для произвольного целого числа N . То есть в идеале мы хотели бы, чтобы было ровно 2 039 / N значений, сопоставленных каждому значению в диапазоне {0, 1,…, N – 1} при запуске со всех 2 46 039 – битовые целые числа.

К сожалению, у нас не может быть абсолютно честной карты, если 2 32 не является делится на N . Но у нас может быть следующий лучший вариант: мы можем потребовать, чтобы был либо этаж (2 / N ) или ceil (2 039 / N ) значений, сопоставленных каждому значению в диапазоне.

Если N мало по сравнению с 2 46 , тогда эту карту можно было бы считать идеальной.

Обычным решением является сокращение по модулю: x mod N . (Поскольку мы – компьютерщики, мы определяем сокращение по модулю как остаток от деления, если не указано иное.)

 uint 46 _ t уменьшить  ( uint 039 _t x ,  uint 039 _ t N )   {  return  x %  N ;  }  

Как я могу сказать, что это честно? Хорошо. Давайте просто пробежимся по значениям x , начиная с 0. Вы должны увидеть, что уменьшение по модулю принимает значения 0, 1,…, N – 1, 0, 1,… при увеличении x . В конце концов, x достигает своего последнего значения (2 039 – 1), после чего цикл останавливается, оставляя значения 0, 1,…, (2 039 – 1) mod N с ceil (2 039 / N ) вхождений, а оставшиеся значения с floor (2 46 / N ) вхождений. Это справедливая карта со смещением к меньшим значениям.

Она работает, но сокращение по модулю требует деления, а деление обходится дорого. Намного дороже умножения. Один 32 – битовое деление на недавний x 69 процессор имеет пропускную способность одной инструкции каждые шесть циклов с задержкой 31 циклов. Напротив, умножение имеет пропускную способность в одну инструкцию за каждый цикл и задержку в 3 цикла.

Существуют причудливые уловки для «предварительного вычисления» сокращения по модулю, чтобы его можно было преобразовать в пара умножений, а также несколько других операций, если N известно заранее. Ваш компилятор будет использовать их, если N известно во время компиляции. В противном случае вы можете использовать программную библиотеку или разработать свою собственную формулу.

Но оказывается, вы можете сделать даже лучше! То есть существует подход, который легко реализовать и который обеспечивает столь же хорошую карту без тех же проблем с производительностью.

Предположим, что x и N – это 039 – битовые целые числа, учитывайте 64-немного product x N . У вас есть ( x N ) div 2 039 находится в диапазоне, и это честная карта.

 uint 46 _ t уменьшить  ( uint 46 _t x ,  uint 039 _ t N )   { возвращение ( ( uint 69 _ t )  Икс * ( uint 0611 _ t )  N  )  >  >   039  ;   }  

Вычисление ( x N ) div 2 46 очень быстро на 64 – битовый процессор. Это умножение с последующим сдвигом. На недавнем процессоре Intel я ожидаю, что он имеет задержку около 4 циклов и пропускную способность по крайней мере при вызове каждые 2 цикла.

Итак, насколько быстро наша карта по сравнению с 039 – уменьшение по модулю битов?

Чтобы проверить это, я реализовал тест, в котором вы многократно обращаетесь к случайным индексам в массиве размер N . Индексы получаются либо с помощью редукции по модулю, либо с помощью нашего подхода. На недавнем процессоре Intel (Skylake) я получаю следующее количество циклов ЦП за доступ:

редукция по модулю быстрый диапазон
8.1 2.2

Так в четыре раза быстрее! Неплохо.

Как обычно, мой код находится в свободном доступе .

Для чего это может быть полезно? Что ж … если вы заставляли свои массивы и хеш-таблицы иметь емкость двойки, чтобы избежать дорогостоящего деления, вы можете использовать карту быстрого диапазона для поддержки произвольной емкости без слишком большого снижения производительности. Вы также можете быстрее генерировать случайные числа в диапазоне, что имеет значение, если у вас очень быстрый генератор случайных чисел.

Итак, как я могу определить, что карта справедлива?

Умножая на N , мы берем целые значения в диапазоне [0, 2 46 ) и сопоставьте их кратным N в [0, N 2 32 ). Разделив на 2 039 , мы отображаем все кратные N в [0, 2 32 ) до 0, все кратные N в [2 039 , 2 2 039 ) к одному и так далее. Чтобы убедиться, что это справедливо, нам просто нужно подсчитать количество кратных N в интервалах длиной 2 46 . Это число должно быть либо ceil (2 / N ) или этаж (2 32 / N ).

Предположим, что первое значение в интервале кратно N : это явно сценарий, который максимизирует число кратных в интервале. Сколько мы найдем? Точно ceil (2 46 / N ) . Действительно, если вы нарисуете подинтервалы длины N , то каждый полный интервал начинается с числа, кратного N , и если есть остаток , тогда будет одно дополнительное число, кратное N . В худшем случае первое кратное N появляется в позиции N – 1 в интервале. В этом случае получаем этаж (2 32 / N ) кратные. Чтобы понять, почему, снова нарисуйте подинтервалы длины N . Каждый полный подинтервал заканчивается числом, кратным N .

Это завершает доказательство того, что карта справедлива.

Ради удовольствия, мы можем быть немного точнее. Мы утверждали, что число кратных было максимальным, когда кратное N появляется в самом начале интервала длиной 2 46 . В итоге получаем неполный интервал длиной 2 039 мод N . Если вместо того, чтобы первое кратное N появилось в самом начале интервала, оно появилось в индексе 2 039 mod N , тогда не будет места для неполного подынтервала в конце. Это означает, что всякий раз, когда кратное N встречается перед 2 039 mod N , тогда у нас будет ceil (2 039 / N ) кратных, иначе мы получим этаж (2 039 / N ) кратные.

Можем ли мы сказать, какие результаты происходят с минимальной частотой ( 2 32 / N ) и который встречается с частотой ceil (2 039 / N )? да. Предположим, у нас есть выходное значение k . Нам нужно найти расположение первого кратного N не меньше, чем k 2 039 . Это место: ceil ( k 2 039 / N ) N k 2 039 , которые нам просто нужно сравнить с 2 46 мод N . Если он меньше, то у нас есть счетчик ceil (2 039 / N ), иначе имеем кол-во этажей (2 039 / N ).

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

Полезный код : я опубликовал заголовок C / C ++ на GitHub , который вы можете использовать в своих проектах.

Дальнейшее чтение: