Извлечение битов с помощью одного умножения


Я видел интересный метод, используемый в ответ до еще вопрос, и хотел бы понять это немного лучше.

нам дано 64-разрядное целое число без знака, и нас интересуют следующие биты:

1.......2.......3.......4.......5.......6.......7.......8.......

в частности, мы хотели бы переместить их на верхние восемь позиций, например:

12345678........................................................

мы не заботимся о значении битов, указанных ., и они не должны быть консервированный.

The решение должен был замаскировать нежелательные биты и умножить результат на 0x2040810204081. Это, как выясняется, делает свое дело.

насколько общим является этот метод? Можно ли использовать этот метод для извлечения любого подмножества битов? Если нет, то как выяснить, работает ли этот метод для определенного набора битов?

наконец, как можно было бы найти (a?) правильный множитель для извлечения данных битов?

5 292

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 не использовался в компьютерных шахматах, и, похоже, нет никакой формальной теории относительно существования и единственности таких магических констант.