Извлечение битов с помощью одного умножения
Я видел интересный метод, используемый в ответ до еще вопрос, и хотел бы понять это немного лучше.
нам дано 64-разрядное целое число без знака, и нас интересуют следующие биты:
1.......2.......3.......4.......5.......6.......7.......8.......
в частности, мы хотели бы переместить их на верхние восемь позиций, например:
12345678........................................................
мы не заботимся о значении битов, указанных .
, и они не должны быть консервированный.
The решение должен был замаскировать нежелательные биты и умножить результат на 0x2040810204081
. Это, как выясняется, делает свое дело.
насколько общим является этот метод? Можно ли использовать этот метод для извлечения любого подмножества битов? Если нет, то как выяснить, работает ли этот метод для определенного набора битов?
наконец, как можно было бы найти (a?) правильный множитель для извлечения данных битов?
5 ответов:
очень интересный вопрос, и хитрый трюк.
давайте рассмотрим простой пример манипулирования одним байтом. Использование без знака 8 бит для простоты. Представьте, что ваш номер
xxaxxbxx
а ты хочешьab000000
.решение состояло из двух этапов: немного маскировки с последующим умножением. Битовая маска-это простая операция, которая превращает неинтересные биты в нули. В приведенном выше случае, ваша маска будет
00100100
и в результате00a00b00
.теперь самое трудное: превратить это в
ab......
.умножение-это набор операций сдвига и сложения. Ключ должен позволить переполнению "сдвинуть" биты, которые нам не нужны, и поместить те, которые мы хотим, в нужное место.
умножение на 4 (
00000100
) сдвинет все, что осталось на 2 и вы получитеa00b0000
. Чтобы получитьb
чтобы двигаться вверх, нам нужно умножить на 1 (чтобы сохранить a в нужном месте) + 4 (чтобы переместить b вверх). Эта сумма составляет 5, и в сочетании с предыдущими 4 мы получаем магическое число 20, или00010100
. Оригинал был00a00b00
после маскировки; умножение дает:000000a00b000000 00000000a00b0000 + ---------------- 000000a0ab0b0000 xxxxxxxxab......
от этого подхода вы можете расширить до больших чисел и больше битов.
один из вопросов, который вы задали, был "Можно ли это сделать с любым количеством битов?"Я думаю, что ответ "нет", если вы не разрешите несколько операций маскировки или несколько умножений. Проблема заключается в вопросе "коллизий" - например, "бродячий b" в задаче выше. Представьте, что мы должны сделать это с числом, как
xaxxbxxcx
. Следуя более раннему подходу, вы могли бы подумать, что нам нужно {x 2, x {1 + 4 + 16}} = х 42 (Оооо-ответ на все!). Результат:00000000a00b00c00 000000a00b00c0000 0000a00b00c000000 ----------------- 0000a0ababcbc0c00 xxxxxxxxabc......
как вы можете видеть, он все еще работает, но "только что". Ключ здесь в том, что между битами, которые мы хотим, есть" достаточно места", чтобы мы могли сжать все. Я не мог добавить четвертый бит d сразу после c, потому что я бы получил экземпляры, где я получаю c+d, биты могут нести, ...
единственное исключение, которое я могу придумать для правила "должен иметь (N-1) нули между битами", это: если вы хотите извлечь два бита, которые соседствуют друг с другом в оригинале, И вы хотите, чтобы они были в том же порядке, тогда вы все еще можете это сделать. И для целей правила (N-1) они считаются двумя битами.
есть еще одно озарение-вдохновленное ответом @Ternary ниже (см. Мой комментарий там). Для каждого интересного бита вам нужно только столько нулей справа от него, сколько вам нужно места для битов, которые должны идти туда. Но также ему нужно столько битов слева, сколько у него есть результат-биты слева. Поэтому, если бит b оказывается в положении m из n, то он должен иметь m-1 нуль слева, и n-m нулей справа. Особенно, когда биты не находятся в том же порядке в исходном номере, что и после переупорядочения, это является важным улучшением исходных критериев. Это означает, например, что 16-битное слово
a...e.b...d..c..
можно переместить в
abcde...........
хотя есть только одно пространство между e и b, два между d и c, три между другими. Что случилось с Н-1?? В этом кейс,
a...e
становится "один блок" - они умножаются на 1, чтобы оказаться в нужном месте, и поэтому"мы получили e бесплатно". То же самое верно для b и d (b нуждается в трех пространствах справа, d нуждается в тех же трех слева). Поэтому, когда мы вычисляем магическое число, мы обнаруживаем, что есть дубликаты:a: << 0 ( x 1 ) b: << 5 ( x 32 ) c: << 11 ( x 2048 ) d: << 5 ( x 32 ) !! duplicate e: << 0 ( x 1 ) !! duplicate
очевидно, что если вы хотите, чтобы эти числа были в другом порядке, вам придется разместить их дальше. Мы можем переформулировать
(N-1)
правило: "он всегда будет работать, если есть хотя бы (N-1) промежутки между битами; или, если порядок битов в конечном результате известен, то если бит b заканчивается в позиции m из n, он должен иметь M-1 нули слева и n-m нулей справа."@Ternary указал, что это правило не совсем работает, так как может быть перенос из битов, добавляющих "только справа от целевой области", а именно, когда биты, которые мы ищем, являются всеми. Продолжая пример, который я дал выше с пятью плотно упакованными битами в 16-битном слове: если начнем с
a...e.b...d..c..
для простоты я назову битовые позиции
ABCDEFGHIJKLMNOP
математика, которую мы собирались сделать, была
ABCDEFGHIJKLMNOP a000e0b000d00c00 0b000d00c0000000 000d00c000000000 00c0000000000000 + ---------------- abcded(b+c)0c0d00c00
до сих пор мы думали, что все, что ниже
abcde
(позицииABCDE
) не имело бы значения, но на самом деле, как указал @Ternary, еслиb=1, c=1, d=1
затем(b+c)
в должностиG
вызовет немного нести в положениеF
, что означает(d+1)
в должностиF
отнесу немного вE
- и наши результат испорчен. Обратите внимание, что пространство справа от наименее значимого бита интереса (c
в этом примере) не имеет значения, так как умножение вызовет заполнение нулями из beyone наименее значимого бита.поэтому нам нужно изменить наше правило (m-1)/(n-m). Если есть более одного бита, который имеет "точно (n-m) неиспользуемые биты справа (не считая последнего бита в шаблоне -" c " в приведенном выше примере), то нам нужно усилить правило - и мы должны это сделать итеративно!
мы должны смотреть не только на количество битов, которые отвечают критерию (n-m), но и на те, которые находятся в (n-m+1) и т. д. Назовем их число Q0 (ровно
n-m
к следующему биту), Q1 (n-m+1), до Q(N-1) (n-1). Тогда мы рискуем снести еслиQ0 > 1 Q0 == 1 && Q1 >= 2 Q0 == 0 && Q1 >= 4 Q0 == 1 && Q1 > 1 && Q2 >=2 ...
если вы посмотрите на это, вы можете увидеть, что если вы пишете простое математическое выражение
W = N * Q0 + (N - 1) * Q1 + ... + Q(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 input:
(set-logic BV) (declare-const mask (_ BitVec 64)) (declare-const multiplicand (_ BitVec 64)) (assert (forall ((x (_ BitVec 64))) (let ((y (bvmul (bvand mask x) multiplicand))) (and (= ((_ extract 63 63) x) ((_ extract 63 63) y)) (= ((_ extract 55 55) x) ((_ extract 62 62) y)) (= ((_ extract 47 47) x) ((_ extract 61 61) y)) (= ((_ extract 39 39) x) ((_ extract 60 60) y)) (= ((_ extract 31 31) x) ((_ extract 59 59) y)) (= ((_ extract 23 23) x) ((_ extract 58 58) y)) (= ((_ extract 15 15) x) ((_ extract 57 57) y)) (= ((_ extract 7 7) x) ((_ extract 56 56) y)) ) ) ) ) (check-sat) (get-model)
а теперь давайте спросим у доказателя теоремы Z3, является ли это теоремой:
z3.exe /m /smt2 ExtractBitsThroughAndWithMultiplication.smt2
результат это:
sat (model (define-fun mask () (_ BitVec 64) #x8080808080808080) (define-fun multiplicand () (_ BitVec 64) #x0002040810204081) )
Бинго! Он воспроизводит результат, указанный в исходном сообщении, за 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
):a00b000c + 0b000c00 + 00c00000 = abc.....
...a.b....c...
терпит неудачу, потому чтоb
находится в 2 битах послеa
(и втягивается в чужое место, когда мы сдвигаемсяa
):a0b0000c + 0b0000c0 + 00c00000 = abX.....
...a...b.c...
Терпит неудачу, потому чтоb
находится в 2 битах, предшествующихc
(и получает толкнул на чужое место, когда мы сдвигаемc
):a000b0c0 + 0b0c0000 + b0c00000 = Xbc.....
...a...bc...d...
работает, потому что последовательные биты сдвигаются:a000bc000d + 0bc000d000 + 000d000000 = abcd000000
но у нас есть проблема. если мы используем тег
n-i
вместоn-1
у нас может быть следующий сценарий: что, если у нас есть столкновение за пределами той части, о которой мы заботимся, что-то, что мы замаскируем в конце, но чьи биты переноса заканчиваются до вмешательства в важный немаскированный диапазон? (и примечание:n-1
требование гарантирует, что этого не произойдет, убедившись, чтоi-1
биты после нашего диапазона без маски ясны, когда мы сдвигаемi
ой)
...a...b..c...d...
потенциальный отказ на перенос битов,c
находится вn-1
послеb
, но отвечаетn-i
критерии:a000b00c000d + 0b00c000d000 + 00c000d00000 + 000d00000000 = abcdX.......
так почему бы нам просто не вернуться к этому"
n-1
бит пространства" требование? потому что мы можем сделать лучше:
...a....b..c...d..
не"n-1
биты пространства " тест, но работает для нашего бит-извлечения трюк:+ a0000b00c000d00 + 0b00c000d000000 + 00c000d00000000 + 000d00000000000 = abcd...0X......
я не могу придумать хороший способ, чтобы охарактеризовать эти поля не есть
n-1
пространство между важными битами, но все равно будет работать для нашей операции. Однако, так как мы знаем заранее биты, которые нас интересуют, мы можем проверить наш фильтр, чтобы убедиться, что мы не опыт переноса битных столкновений:сравнить
(-1 AND mask) * shift
против ожидаемого результата all-ones,-1 << (64-n)
(для 64-разрядной версии без знака)магический сдвиг / умножение для извлечения наших битов работает тогда и только тогда, когда они равны.
в дополнение к уже отличным ответам на этот очень интересный вопрос, было бы полезно знать, что этот трюк побитового умножения был известен в компьютерном шахматном сообществе с 2007 года, где он идет под названием Магия BitBoards.
многие компьютерные шахматные движки используют несколько 64-разрядных целых чисел (называемых битбордами) для представления различных наборов фигур (1 бит на занятый квадрат). Предположим, что скользящая фигура (ладья, слон, Королева) на определенном исходном квадрате может двигаться не более
K
квадраты, если никаких блокирующих частей не было. Используя побитовое-и из тех разбросанныхK
биты с bitboard занимаемых квадратов дает конкретныеK
-битовое слово, встроенное в 64-разрядное целое число.магическое умножение может быть использовано для отображения этих рассеянных
K
бит до нижнегоK
биты 64-разрядного целого числа. Эти нижеK
биты могут быть использованы для индексирования таблицы предварительно вычисленных битовых панелей, которые представляет разрешенные квадраты, на которые может фактически переместиться фигура на исходном квадрате (заботясь о блокировке фигур и т. д.)типичный шахматный движок, использующий этот подход, имеет 2 таблицы (одну для ладей, одну для слонов, ферзей, использующих комбинацию обоих) из 64 записей (по одной на исходный Квадрат), которые содержат такие предварительно вычисленные результаты. Оба самых высоких номинальных закрытых источника (Гудини) и шахматный движок с открытым исходным кодом (Stockfish) в настоящее время используют этот подход для очень высокой производительности.
поиск этих магических множителей выполняется либо с помощью исчерпывающий поиск (оптимизировано с ранними отсечениями) или с проб и ошибок (например, пробует много случайных 64-разрядных целых чисел). Не было никаких битовых шаблонов, используемых во время генерации движения, для которых не было найдено никакой магической константы. Тем не менее, побитовый перенос эффекты обычно необходимы, когда подлежащие отображению биты имеют (почти) смежные индексы.
AFAIK, очень общий подход SAT-решателя @Syzygy не использовался в компьютерных шахматах, и, похоже, нет никакой формальной теории относительно существования и единственности таких магических констант.