Я видел интересную технику, использованную в ответе на другой вопрос , и хотел бы понять ее немного лучше.
Нам дано 64-разрядное целое число без знака, и нас интересуют следующие биты:
1.......2.......3.......4.......5.......6.......7.......8.......
В частности, мы хотели бы переместить их в верхние восемь позиций, вот так:
12345678........................................................
Нас не волнует значение битов, обозначенных как .
, и они не должны быть сохранены.
Решение было замаскировать нежелательные биты, и умножить результат на 0x2040810204081
. Это, как оказывается, делает свое дело.
Насколько общий этот метод? Можно ли использовать эту технику для извлечения какого-либо подмножества битов? Если нет, то как выяснить, работает ли метод для определенного набора битов?
Наконец, как можно найти (a?) Правильный множитель для извлечения заданных битов?
Ответы:
Очень интересный вопрос и хитрый трюк.
Давайте посмотрим на простой пример манипулирования одним байтом. Использование 8-битного без знака для простоты. Представь, что твой номер есть
xxaxxbxx
и ты хочешьab000000
.Решение состояло из двух этапов: немного маскировки с последующим умножением. Битовая маска - это простая операция И, которая превращает неинтересные биты в нули. В приведенном выше случае, ваша маска будет
00100100
и результат00a00b00
.Теперь самое сложное: превратить это в
ab......
.Умножение - это набор операций сдвига и сложения. Ключ должен позволить переполнению «сдвинуть» ненужные нам биты и поместить нужные нам в нужное место.
Умножение на 4 (
00000100
) сместит все, что осталось, на 2 и приведет вас кa00b0000
. Чтобы заставитьb
двигаться вверх, нам нужно умножить на 1 (чтобы держать a в нужном месте) + 4 (чтобы переместить b вверх). Эта сумма равна 5, и в сочетании с предыдущими 4 мы получаем магическое число 20 или00010100
. Оригинал был00a00b00
после маскировки; умножение дает:При таком подходе вы можете расширить до больших чисел и больше битов.
Один из заданных вами вопросов был: «Можно ли это сделать с любым количеством бит?» Я думаю, что ответ «нет», если вы не разрешаете несколько операций маскирования или несколько умножений. Проблема в проблеме «столкновений» - например, «заблудившийся б» в задаче выше. Представьте, что нам нужно сделать это с таким числом, как
xaxxbxxcx
. Следуя более раннему подходу, вы можете подумать, что нам нужно {x 2, x {1 + 4 + 16}} = x 42 (оооо - ответ на все вопросы!). Результат:Как видите, он все еще работает, но «только что». Ключевым моментом здесь является то, что между битами, которые мы хотим, есть «достаточно места», чтобы мы могли все сжать. Я не мог добавить четвертый бит d сразу после c, потому что я получал случаи, когда я получал c + d, биты могли нести, ...
Поэтому без формального доказательства я бы ответил на более интересные части вашего вопроса следующим образом: «Нет, это не будет работать для любого количества бит. Чтобы извлечь N бит, вам нужно (N-1) пробелы между битами, которые вы хотите извлекать или иметь дополнительные шаги умножения маски. "
Единственное исключение, которое я могу придумать для правила «должны иметь (N-1) нули между битами», заключается в следующем: если вы хотите извлечь два бита, смежных друг с другом в оригинале, и хотите сохранить их в тот же порядок, тогда вы все еще можете сделать это. А для правила (N-1) они считаются двумя битами.
Есть еще одна идея, вдохновленная ответом @Ternary ниже (см. Мой комментарий там). Для каждого интересного бита вам нужно только столько нулей справа от него, сколько вам нужно места для битов, которые должны идти туда. Но также нужно столько же битов слева, сколько и битов результата слева. Таким образом, если бит b оказывается в позиции m из n, то он должен иметь нули m-1 слева и нули nm справа. Особенно, когда биты не находятся в том же порядке в исходном номере, как они будут после переупорядочения, это является важным улучшением к первоначальным критериям. Это означает, например, что 16-битное слово
Может быть сдвинут в
хотя между e и b есть только один пробел, два между d и c, три между остальными. Что случилось с N-1? В этом случае
a...e
становится «один блок» - они умножаются на 1, чтобы оказаться в нужном месте, и поэтому «мы получили е бесплатно». То же самое верно для b и d (b нужно три пробела справа, d нужно те же три слева). Поэтому, когда мы вычисляем магическое число, мы обнаруживаем, что есть дубликаты:Понятно, что если вы хотите, чтобы эти числа были в другом порядке, вам пришлось бы расположить их дальше. Мы можем переформулировать
(N-1)
правило: «Оно всегда будет работать, если между битами будет не менее (N-1) пробелов, или, если известен порядок битов в конечном результате, то если бит b окажется в позиции m n, он должен иметь нули m-1 слева и нули nm справа ".@Ternary указал, что это правило не совсем работает, так как может быть переход от битов, добавляющих «только справа от целевой области», а именно, когда все искомые биты являются единицами. Продолжая пример, который я привел выше, с пятью плотно упакованными битами в 16-битном слове: если мы начнем с
Для простоты я назову битовые позиции
ABCDEFGHIJKLMNOP
Математика, которую мы собирались сделать, была
До сих пор мы думали, что что-либо ниже
abcde
(позицииABCDE
) не будет иметь значения, но на самом деле, как указывало @Ternary, еслиb=1, c=1, d=1
тогда(b+c)
в позицииG
будет немного переходить в позициюF
, что означает, что(d+1)
в позицииF
будет немного переходитьE
- и наша результат испорчен. Обратите внимание, что пробел справа от наименее значимого бита, представляющего интерес (c
в этом примере), не имеет значения, так как умножение приведет к заполнению нулями от любого младшего значащего бита.Поэтому нам нужно изменить наше правило (m-1) / (nm). Если имеется более одного бита, который имеет «точно (нм) неиспользуемые биты справа (не считая последний бит в шаблоне -« с »в примере выше), то нам нужно усилить правило - и мы должны делать это итеративно!
Мы должны смотреть не только на число битов, которые соответствуют критерию (нм), но также и на те, которые имеют (n-m + 1) и т. Д. Давайте назовем их число Q0 (точно
n-m
следующим битом), Q1 ( n-m + 1), до Q (N-1) (n-1). Тогда мы рискуем нести, еслиЕсли вы посмотрите на это, вы увидите, что если вы напишите простое математическое выражение
и в результате
W > 2 * N
вы должны увеличить критерий RHS на один бит до(n-m+1)
. На данный момент, операция безопасна, покаW < 4
; если это не сработает, увеличьте критерий еще раз и т. д.Я думаю, что следуя вышесказанному, вы получите длинный путь к вашему ответу ...
источник
Действительно очень интересный вопрос. Я согласен с моими двумя центами, а именно: если вам удастся сформулировать подобные проблемы с точки зрения логики первого порядка над теорией битвекторов, то доказатели теорем - ваш друг, и потенциально могут предоставить вам очень быстро ответы на ваши вопросы. Давайте вновь сформулируем проблему, которую задают в качестве теоремы:
"Существует несколько 64-битных констант 'mask' и 'multiplicand', так что для всех 64-битных битовых векторов x в выражении y = (x & mask) * multiplicand мы имеем, что y.63 == x.63 , y.62 == x.55, y.61 == x.47 и т. д. "
Если это предложение на самом деле является теоремой, то верно, что некоторые значения констант «маска» и «умножение» удовлетворяют этому свойству. Итак, давайте сформулируем это в терминах чего-то, что может понять доказатель теоремы, а именно ввода SMT-LIB 2:
А теперь давайте спросим доказателя теорем Z3, является ли это теоремой:
Результат:
Бинго! Воспроизводит результат, указанный в оригинальном сообщении, за 0,06 секунды.
Рассматривая это в более общей перспективе, мы можем рассматривать это как пример проблемы синтеза программ первого порядка, которая является зарождающейся областью исследований, о которой было опубликовано несколько статей. Поиск
"program synthesis" filetype:pdf
должен начать вас.источник
Каждый 1-бит в множителе используется для копирования одного из битов в правильную позицию:
1
уже в правильном положении, поэтому умножьте на0x0000000000000001
.2
необходимо сдвинуть 7-битные позиции влево, поэтому мы умножаем на0x0000000000000080
(бит 7 установлен).3
необходимо сдвинуть 14-битные позиции влево, поэтому мы умножаем на0x0000000000000400
(бит 14 установлен).8
необходимо сместить 49-битные позиции влево, поэтому мы умножаем на0x0002000000000000
(бит 49 установлен).Множитель является суммой множителей для отдельных битов.
Это работает только потому, что собираемые биты не слишком близки друг к другу, так что умножение битов, которые не принадлежат друг другу в нашей схеме, либо выходит за пределы 64-битной, либо в нижней части, не требующей обслуживания.
Обратите внимание, что остальные биты в оригинальном номере должны быть
0
. Это может быть достигнуто путем маскировки их операцией AND.источник
(Я никогда не видел это раньше. Этот трюк великолепен!)
Я немного расширю утверждение Флориса о том, что при извлечении
n
битов вам нужноn-1
пространство между любыми непоследовательными битами:Моя первоначальная мысль (через минуту мы увидим, как это не совсем работает) заключалась в том, что вы могли бы добиться большего: если вы хотите извлечь
n
биты, у васi
возникнет коллизия при извлечении / сдвиге битов, если у вас есть кто-либо (не -последовательный с битомi
) вi-1
предыдущихn-i
битах или последующих битах.Я приведу несколько примеров для иллюстрации:
...a..b...c...
Работает (никто в 2 битах послеa
, бит до и немного послеb
, и никто не в 2 битах доc
):...a.b....c...
Сбой, потому чтоb
находится в 2 битах послеa
(и попадает в чужое место, когда мы сдвигаемсяa
):...a...b.c...
Сбой, потому чтоb
находится в 2 битах предшествующихc
(и выталкивается в чужое место, когда мы сдвигаемсяc
):...a...bc...d...
Работает, потому что последовательные биты сдвигаются вместе:Но у нас есть проблема. Если бы мы использовали
n-i
вместо этого, уn-1
нас мог бы быть следующий сценарий: что, если у нас будет столкновение вне части, о которой мы заботимся, что-то, что мы бы замаскировали в конце, но чьи биты переноса заканчивают тем, что вмешивались в важный немаскированный диапазон ? (и обратите внимание:n-1
требование гарантирует, что этого не произойдет, убедившись, чтоi-1
биты после нашего немаскированного диапазона очищены, когда мы сдвигаемi
бит th)...a...b..c...d...
Потенциальный отказ от перенесенных битов,c
вn-1
послеb
, но удовлетворяетn-i
критериям:Так почему бы нам просто не вернуться к этому требованию "
n-1
бит пространства"? Потому что мы можем сделать лучше :...a....b..c...d..
Сбойn-1
теста « биты места», но работает для нашего трюка по извлечению битов:Я не могу придумать хороший способ охарактеризовать эти поля, которые не имеют
n-1
пробелов между важными битами, но все же будут работать для нашей работы. Однако, поскольку мы заранее знаем, какие биты нам интересны, мы можем проверить наш фильтр, чтобы убедиться, что мы не столкнулись с битами переноса:Сравните
(-1 AND mask) * shift
с ожидаемым результатом «все единицы»-1 << (64-n)
(для 64-разрядных без знака)Волшебный сдвиг / умножение для извлечения наших битов работает тогда и только тогда, когда они равны.
источник
b
заканчивается в положенииm
оn
, то она должна иметьm-1
нули слева от него , иn-m-1
нули справа от него . Особенно, когда биты не находятся в том же порядке в исходном номере, как они будут после переупорядочения, это является важным улучшением к первоначальным критериям. Это весело.В дополнение к и без того отличным ответам на этот очень интересный вопрос, возможно, было бы полезно знать, что этот прием побитового умножения известен в сообществе компьютерных шахмат с 2007 года, где он носит название Magic BitBoards .
Многие компьютерные шахматные движки используют несколько 64-битных целых чисел (называемых битбордами) для представления различных наборов фигур (1 бит на занятый квадрат). Предположим, что скользящая фигура (ладья, слон, королева) на определенном исходном квадрате может переместиться на самое большее число
K
квадратов, если не было блокирующих фигур. Использование побитового и этих разбросанныхK
битов с битовой доской из занятых квадратов дает конкретноеK
-битное слово, встроенное в 64-битное целое число.Волшебное умножение можно использовать для сопоставления этих разбросанных
K
битов с младшимиK
битами 64-битного целого числа. Эти младшиеK
биты могут затем использоваться для индексации таблицы предварительно вычисленных битовых досок, которая представляет разрешенные квадраты, к которым может фактически перемещаться фрагмент на его исходном квадрате (заботясь о блокирующих фрагментах и т. Д.)Типичный шахматный движок, использующий этот подход, имеет 2 таблицы (одну для ладей, одну для епископов, ферзей, использующих комбинацию обоих) из 64 записей (одна для квадрата происхождения), которые содержат такие предварительно вычисленные результаты. И закрытый исходный код с самым высоким рейтингом ( Houdini ), и шахматный движок с открытым исходным кодом ( Stockfish ) в настоящее время используют этот подход для своей очень высокой производительности.
Поиск этих магических множителей выполняется либо с использованием исчерпывающего поиска (оптимизированного с ранними отсечками), либо с помощью метода trial и erorr (например, при попытке получить много случайных 64-битных целых чисел). Во время генерации хода не использовались битовые комбинации, для которых не удалось найти магическую постоянную. Однако побитовые эффекты переноса обычно необходимы, когда отображаемые биты имеют (почти) смежные индексы.
AFAIK, очень общий подход SAT-решателя @Syzygy, не использовался в компьютерных шахматах, и при этом нет никакой формальной теории относительно существования и уникальности таких магических констант.
источник