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

285

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

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

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

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

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

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

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

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

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

  • 28
    Если вы нашли это интересное, взгляните на этот список: graphics.stanford.edu/~seander/bithacks.html Многие из них (ab) используют более широкое целочисленное умножение / деление для достижения интересных результатов. (Часть «Реверсировать биты в байте с 4 операциями» показывает, как справиться с уловкой сдвига / умножения битов, когда вам не хватает места и вам нужно дважды / замаскировать / умножить)
  • 0
    @viraptor: Отличная мысль. Если вы понимаете ограничения этого метода, вы действительно можете использовать умножение, чтобы добиться многого в отношении битовых манипуляций.
Показать ещё 5 комментариев
Теги:
bit-manipulation
multiplication

5 ответов

216
Лучший ответ

Очень интересный вопрос и умный трюк.

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

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

Теперь сложная часть: превращение этого в ab.......

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

Умножение на 4 (00000100) сдвинет все оставшееся на 2 и приведет вас к a00b0000. Чтобы ускорить движение b, нам нужно умножить на 1 (чтобы сохранить a в нужном месте) + 4 (чтобы переместить b вверх). Эта сумма равна 5, и в сочетании с предыдущими 4 мы получаем магическое число 20 или 00010100. Оригинал был 00a00b00 после маскировки; умножение дает:

000000a00b000000
00000000a00b0000 +
----------------
000000a0ab0b0000
xxxxxxxxab......

Из этого подхода вы можете перейти к большему количеству и количеству бит.

Один из вопросов, который вы задавали, "может ли это быть сделано с любым количеством бит?" Я думаю, что ответ "нет", если вы не разрешаете несколько операций маскировки или несколько умножений. Проблема заключается в проблеме "коллизий" - например, "бродячая б" в проблеме выше. Представьте себе, что нам нужно сделать это с таким числом, как 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...........

хотя между e и b существует только одно пространство, два между d и c, три между другими. Что бы ни случилось с N-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 нулей слева, а nm нули справа."

@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) правило. Если имеется более одного бита, который имеет "точно (нм) неиспользуемые биты справа (не считая последний бит в шаблоне -" c "в приведенном выше примере), тогда нам нужно усилить правило - и нам нужно делайте это итеративно!

Мы должны смотреть не только на число битов, удовлетворяющих критерию (nm), но и на то, что находится в (n-m + 1) и т.д. Пусть назовем их число Q0 (точно n-m to следующий бит), 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; если это не сработает, увеличьте критерий еще один и т.д.

Я думаю, что после вышесказанного вам предстоит пройти долгий путь...

  • 1
    Отлично. Еще одна тонкая проблема: тест m-1 / nm иногда терпит неудачу из-за битов переноса. Попробуйте a ... b..c ... d - вы получите b + c в пятом бите, который, если они оба равны 1, создает бит переноса, который сжимает d (!)
  • 1
    результат: n-1 бит запрещает конфигурации, которые должны работать (то есть ... b..c ... d), а m-1 / nm разрешает те, которые не работают (a ... b..c ... d). Я не смог придумать простой способ охарактеризовать, что будет работать, а что нет.
Показать ещё 3 комментария
154

Очень интересный вопрос. Я перебираю мои два цента, а это значит, что если вы можете сформулировать такие проблемы, как в случае логики первого порядка над теорией битвектора, то теоретические проусоры - ваш друг и потенциально могут обеспечить вас очень быстро ответы на ваши вопросы. Пусть переформулирует задание как теорему:

"Существует некоторая 64-битная константа 'mask' и 'multipicand', так что для всех 64-разрядных битов векторов x в выражении y = (x и mask) * multipicand мы имеем, что 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 должен начать вас.

  • 2
    Я впечатлен! Я не знал, что «логика первого порядка над теорией битовых векторов» была даже реальным предметом, который изучали люди - не говоря уже о том, что он мог дать такие интересные результаты. Большое спасибо за то, что поделились этим.
  • 0
    @AndrewBacker: Может ли кто-то осветить меня относительно того, что есть в этом так называемом «так как работа»? Я имею в виду, это ничего не платит . Вы не можете жить в одиночку. Может быть, это может дать вам несколько очков в интервью. Может быть. Если на рабочем месте достаточно хорошо, чтобы признать ценность SO респ, и это не дано ...
Показать ещё 1 комментарий
80

Каждый 1-бит в множителе используется для копирования одного из битов в правильное положение:

  • 1 уже находится в правильном положении, поэтому умножьте на 0x0000000000000001.
  • 2 необходимо сдвинуть 7-битные позиции влево, поэтому мы умножим на 0x0000000000000080 (бит 7 установлен).
  • 3 должно быть сдвинуто на 14 бит влево, поэтому умножим на 0x0000000000000400 (бит 14 установлен).
  • и т.д., пока
  • 8 должно быть сдвинуто на 49 бит влево, поэтому умножим на 0x0002000000000000 (бит 49 установлен).

Умножитель представляет собой сумму умножителей для отдельных битов.

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

Обратите внимание, что остальные биты исходного номера должны быть 0. Это может быть достигнуто путем маскировки их с помощью операции И.

  • 2
    Отличное объяснение! Ваш короткий ответ позволил быстро найти значение «магического числа».
  • 3
    Это действительно лучший ответ, но он не был бы таким полезным, если бы сначала не прочитал (первую половину) ответ @ floris.
28

(Я никогда раньше этого не видел. Этот трюк замечательный!)

Я немного расскажу о утверждении Флориса о том, что при извлечении битов n вам нужно n-1 пробел между любыми нескончаемыми битами:

Моя первоначальная мысль (мы увидим через минуту, как это не совсем работает) заключается в том, что вы можете сделать лучше: если вы хотите извлечь биты n, у вас будет столкновение при извлечении/смещении бит i, если у вас есть кто-то (не последовательный с битом i) в бит i-1, предшествующий или n-i бит, следующий.

Я приведу несколько примеров для иллюстрации:

...a..b...c... Работает (никто из 2-х бит после a, бит до и бит после b, и никто не находится в двух битах до 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 th)

...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-разрядного без знака)

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

  • 0
    Мне это нравится - вы правы в том, что для каждого бита вам нужно только столько нулей справа от него, сколько вам нужно места для битов, которые должны идти туда. Но также , ему нужно столько бит слева, сколько битов результата слева. Таким образом, если бит b оказывается в позиции m из n , то он должен иметь нули m-1 слева и нули nm-1 справа. Особенно, когда биты не находятся в том же порядке в исходном номере, как они будут после переупорядочения, это является важным улучшением к первоначальным критериям. Это весело.
12

В дополнение к уже отличным ответам на этот очень интересный вопрос, было бы полезно знать, что этот побитовый трюк умножения известен в компьютерном шахматном сообществе с 2007 года, где он находится под именем Magic BitBoards.

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

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

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

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

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

  • 0
    Я бы подумал, что любой, кто имеет полноценный формальный опыт работы с CS, сразу же столкнется с подходом SAT, увидев эту проблему. Может быть, CS люди считают шахматы неинтересными :(
  • 0
    @KubaOber В основном все наоборот: в компьютерных шахматах преобладают бит-тиддлеры, которые программируют на C или ассемблере и ненавидят любые абстракции (C ++, шаблоны, OO). Я думаю, что это отпугивает настоящих ребят из CS :-)

Ещё вопросы

Сообщество Overcoder
Наверх
Меню