Извлечение битов одним умножением

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

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

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

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

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

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

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

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

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

5 ответов


очень интересный вопрос, и хитрый трюк.

давайте рассмотрим простой пример манипулирования одним байтом. Использование unsigned 8 бит для простоты. Представьте, что ваш номер xxaxxbxx и вы хотите ab000000.

решение состояло из двух шагов: немного маскировки с последующим умножением. Битовая маска-это простая операция, которая превращает неинтересные биты в нули. В приведенном выше случае, ваша маска будет 00100100 и в результате 00a00b00.

теперь трудная часть: превращение этого в ab.......

умножение-это куча операций сдвига и добавления. Ключ должен позволить overflow "сдвинуть" биты, которые нам не нужны, и поместить те, которые мы хотим, в нужное место.

умножение на 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}} = x 42 (oooh - ответ на все!). Результат:

00000000a00b00c00
000000a00b00c0000
0000a00b00c000000
-----------------
0000a0ababcbc0c00
xxxxxxxxabc......

как вы можете видеть, он все еще работает, но "только". Они ключ здесь в том, что есть" достаточно места " между битами, которые мы хотим, чтобы мы могли сжать все. Я не мог добавить четвертый бит d сразу после c, потому что я получал бы экземпляры, где я получаю c+d, биты могут нести, ...

поэтому без формального доказательства я бы ответил на более интересные части вашего вопроса следующим образом: "нет, это не будет работать для любого количества битов. Чтобы извлечь N битов, вам нужны (N-1) пробелы между битами, которые вы хотите извлечь, или дополнительные шаги умножения маски."

единственное исключение, которое я могу придумать для Правила "должно иметь (N-1) нули между битами", таково: если вы хотите извлечь два бита, которые соседствуют друг с другом в оригинале, И вы хотите держать их в том же порядке, тогда вы все еще можете это сделать. И для целей правила (N-1) они считаются двумя битами.

есть еще одно понимание-вдохновленный ответом @Ternary ниже (см. Мой комментарий там). Для каждого интересного бита вам нужно только столько нулей справа от него, сколько вам нужно места для битов, которые должны идти туда. Но также ему нужно столько бит слева, сколько у него есть результат-бит слева. Поэтому, если бит b оказывается в позиции m из n, то он должно быть m-1 нулей слева и n-m нулей справа. Особенно, когда биты не находятся в том же порядке в исходном номере, что и после переупорядочения, это является важным улучшением исходных критериев. Это означает, например, что 16-битное слово

a...e.b...d..c..

можно перенести в

abcde...........
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 в этом примере) не имеет значения, так как умножение вызовет заполнение нулями из Бейона наименее значимого бита.

поэтому нам нужно изменить наше(m-1)/(n-m) правило. Если есть более одного бита, который имеет "точно (n-m) неиспользуемые биты справа (не считая последнего бита в шаблоне -" c " в приведенном выше примере), то нам нужно укрепить правило - и мы должны это сделать итеративно!

мы должны смотреть не только на количество битов, удовлетворяющих критерию (n-m), но и на те, которые находятся в (n-m+1) и т. д. Назовем их номер Q0 (ровно n-m до следующего бита), Q1 (n-m+1), до Q(N-1) (n-1). Тогда мы рискуем пронести if

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 и маска) * множимое, мы имеем, что y.63 == х.63, г.62 == х.55, г.61 == х.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)

а теперь давайте спросим теорему prover 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 bits, у вас будет столкновение при извлечении / сдвиге бит 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... потенциальный отказ на carry-bits,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 бит на занятый квадрат). Предположим, скользящая часть (ладья, епископ, queen) на определенном квадрате происхождения может двигаться не более K квадраты, если не было блокирующих частей. Используя побитовые-и из тех, что разбросаны K бит с битовой доской занятых квадратов дает определенный K-разрядное слово, встроенное в 64-разрядное целое число.

магическое умножение может быть использовано для отображения этих рассеянных K бит к нижнему K биты 64-разрядного целого числа. Эти ниже K биты затем могут использоваться для индексации таблицы предварительно вычисленных битовых панелей, которые representst разрешенные квадраты, которые кусок на его исходном квадрате может фактически переместить (заботясь о блокировке частей и т. д.)

типичный шахматный движок, использующий этот подход, имеет 2 таблицы (одну для ладей, одну для епископов, Королев, использующих комбинацию обоих) из 64 записей (по одной на квадрат начала координат), которые содержат такие предварительно вычисленные результаты. Оба самых высоких номинальных закрытых источника (Гудини) и шахматный движок с открытым исходным кодом (Stockfish) в настоящее время использует этот подход для своей очень высокой эффективности.

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

AFAIK, очень общий подход SAT-решателя @Syzygy не использовался в компьютерных шахматах, и, похоже, нет никакой формальной теории относительно существования и единственности таких магических констант.