Вилучення бітів з одним множенням


301

Я побачив цікаву техніку, використану у відповіді на інше питання , і хотів би зрозуміти її трохи краще.

Нам дається 64-розрядне ціле число без підпису, і нас цікавлять наступні біти:

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

Зокрема, ми хотіли б перемістити їх на перші вісім позицій:

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

Нас не хвилює значення бітів, вказаних ., і їх не потрібно зберігати.

Рішення було замаскувати небажані біти, і помножити результат на 0x2040810204081. Це, як виявляється, робить трюк.

Наскільки загальним є цей метод? Чи можна використовувати цю техніку для вилучення будь-якого набору бітів? Якщо ні, то як з'ясовується, чи працює метод для певного набору бітів?

Нарешті, як би йти на пошук правильного множника (a?) Для вилучення заданих бітів?


29
Якщо ви виявили, що цікаво, перегляньте цей список: graphics.stanford.edu/~seander/bithacks.html Багато з них (ab) використовують для більш цікавих результатів множення / поділ ширшого цілого числа. (Частина "Зворотні біти в байті за допомогою 4-х операцій" показує, як поводитися з трюком розмноження / множення, коли у вас недостатньо місця та потрібно маскувати /
множувати

@viraptor: Відмінний момент. Якщо ви розумієте обмеження цього методу, ви можете реально використовувати множення, щоб досягти багато чого щодо маніпулювання бітами.
Expedito

9
Настільки цікаво, що в AVX2 є інструкція (яка, на жаль, ще не доступна), яка виконує саме ту процедуру, яку ви описуєте: software.intel.com/sites/products/documentation/studio/composer/…
JPvdMerwe

3
Ще одне місце для пошуку розумних алгоритмів розкрутки
Barmar

1
Um LIVRO дие conheço Sobre про Assunto (е Gosto bastante) Ео "Хакер Delight" посилання
Саллес

Відповіді:


235

Дуже цікаве запитання та розумна хитрість.

Давайте розглянемо простий приклад маніпулювання одним байтом. Використання неподписаного 8-бітного для простоти. Уявіть, що ваш номер є xxaxxbxxі ви хочете ab000000.

Розчин складався з двох етапів: трохи маскування з подальшим множенням. Бітова маска - це проста операція AND, яка перетворює нецікаві біти на нулі. У наведеному випадку ваша маска була б 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 (ооо - відповідь на все!). Результат:

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

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

Тож без офіційного підтвердження я відповів би на більш цікаві частини вашого запитання наступним чином: "Ні, це не працюватиме для будь-якої кількості біт. Щоб витягти N біт, вам потрібно (N-1) пробіли між бітами, які ви хочете витягнути або зробити додаткові кроки множення маски. "

Єдиний виняток, який я можу придумати для правила "must have (N-1) нулів між бітами", це таке: якщо ви хочете витягнути два біти, які сусідять один з одним в оригіналі, і ви хочете тримати їх у те саме замовлення, то ви все одно можете це зробити. І для норми (N-1) вони рахуються як два біти.

Є ще одне розуміння - натхнене відповіддю @Ternary нижче (дивіться мій коментар там). На кожен цікавий біт вам потрібно лише стільки нулів праворуч від нього, скільки вам потрібно місця для біт, які потрібно туди перейти. Але також йому потрібно стільки ж бітів ліворуч, скільки має біт результатів ліворуч. Отже, якщо біт b закінчується в положенні m n, тоді йому потрібно мати нулі m-1 зліва, а nm нулі праворуч. Особливо, коли біти не в такому ж порядку в початковій кількості, як вони будуть після повторного замовлення, це важливе поліпшення вихідних критеріїв. Це означає, наприклад, що 16-бітове слово

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

Можна перемістити в

abcde...........

навіть незважаючи на те, що між e і b є лише один пробіл, два між d і c, три між іншими. Що б не сталося з N-1 ?? У цьому випадку a...eстають "одним блоком" - вони множать на 1, щоб у кінцевому підсумку потрапити в потрібне місце, і так "ми отримали е безкоштовно". Те ж саме стосується 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 нулі праворуч ".

@ Тернарі зазначив, що це правило не дуже працює, оскільки можна перенести з бітів, додавши "праворуч від цільової області", а саме, коли біти, які ми шукаємо, - це всі. Продовжуючи приклад, який я наводив вище з п'ятьма щільно упакованими бітами в 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) / (nm) правило. Якщо є більше одного біта, який має "точно (nm) невикористані біти праворуч (не рахуючи останній біт у шаблоні -" c "у прикладі вище), тоді нам потрібно посилити правило - і ми повинні зробіть це ітеративно!

Ми повинні дивитися не лише на кількість бітів, що відповідають критерію (nm), але й на ті, які знаходяться у (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, тоді вам потрібно збільшити критерій РЗС на один біт до (n-m+1). На даний момент операція безпечна до тих пір, поки W < 4; якщо це не працює, ще більше збільште критерій тощо.

Я думаю, що слідування вищесказаного дасть вам довгий шлях до вашої відповіді ...


1
Чудово. Ще одна тонка проблема: тест m-1 / nm деякий час виходить з ладу через носії бітів. Спробуйте ... b..c ... d - ви закінчите з b + c в п'ятому біті, який, якщо вони обоє 1, робить переносний шматочок, що клобери d (!)
Тернар

1
підсумок: n-1 біт простору забороняє конфігурації, які повинні працювати (тобто ... b..c ... d), а m-1 / nm дозволяє тим, хто не працює (a ... b..c ... г). Мені не вдалося придумати простий спосіб визначити, який буде працювати, а який - ні.
Тернар

Ти хороший! Проблема з переносом означає, що нам потрібно трохи більше місця праворуч від кожного біта, як «захист». На перший погляд, якщо є щонайменше два біти, які мають саме мінімальний нм праворуч, потрібно збільшити простір на 1. Більш загально, якщо є P таких бітів, вам потрібні додаткові біти log2 (P) до право будь-якого, хто мав мінімум (mn). Вам здається?
Флоріс

Добре, що останній коментар був надто спрощеним. Я думаю, що моя нещодавно відредагована відповідь показує, що log2 (P) - це не правильний підхід. @ Власна відповідь Тернарі (нижче) вишукано показує, як ви можете розказати для певної комбінації бітів, якщо у вас немає гарантованого рішення - я вважаю, що робота вище розроблена на цьому ще дещо.
Флоріс

1
Це, мабуть, збіг обставин, але ця відповідь була прийнята, коли кількість звернень досягла 127. Якщо ви прочитали це далеко, ви посміхніться зі мною ...
Флоріс,

154

Дійсно дуже цікаве питання. Я підказую свої два центи, тобто те, що якщо вам вдасться викласти подібні проблеми з точки зору логіки першого порядку над теорією бітвектора, то докази теореми є вашим другом і потенційно можуть забезпечити вас дуже швидко відповіді на ваші запитання. Давайте ще раз викладемо задачу, яку задають як теорему:

"Існує кілька 64-бітових констант" маска "і" мультиплікація ", так що для всіх 64-бітних бітвекторів x у виразі y = (x & mask) * мультиплікація у нас є, що y.63 == x.63 , y.62 == x.55, y.61 == x.47 тощо "

Якщо це речення насправді є теоремою, то це правда, що деякі значення констант «маска» та «мультиплікація» задовольняють цій властивості. Тож давайте сформулюємо це через те, що може зрозуміти доказ теореми, а саме вхід SMT-LIB 2:

(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
Я перебуваю під враженням! Я не знав, що "логіка першого порядку над теорією бітвектора" була навіть реальною темою, яку вивчали люди - не кажучи вже про те, що вона може дати такі цікаві результати. Дуже дякую, що поділилися цим.
Флоріс

@AndrewBacker: Чи може хтось висвітлити мене щодо того, що є в цій так званій річці "SO-as-a-job"? Я маю на увазі, це нічого не платить . Ви не можете жити на SO представниках самостійно. Можливо, це може дати вам деякі моменти в інтерв'ю. Можливо. Якщо робоче місце досить добре, щоб розпізнати значення показника SO, а це не дано ...
Поновіть Моніку

3
Звичайно. ТАК це також гра (що завгодно з очками) для багатьох людей. Просто людська природа, як полювання на / r / new, щоб ви могли опублікувати перший коментар та отримати карму. Нічого поганого в цьому, поки відповіді ще добрі. Я просто щасливіший, коли зможу підняти чийсь час і сили, коли вони, ймовірно, дійсно помітять, що хтось це зробив. Заохочення - це хороший матеріал :) І ... це був дійсно старий коментар, і все-таки правдивий. Я не бачу, як це не ясно.
Ендрю Бекер

88

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

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

Множник - це сума множників для окремих біт.

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

Зверніть увагу, що інші біти у вихідному номері повинні бути 0. Цього можна досягти, маскуючи їх операцією AND.


2
Чудове пояснення! Ваша коротка відповідь дала можливість швидко знайти значення "магічного числа".
Expedito

4
Це дійсно найкраща відповідь, але це не було б так корисно, не читаючи (перша половина) відповіді @ floris.
Ендрю Бекер

29

(Я ніколи цього не бачив. Цей трюк чудовий!)

Я трохи розкрию твердження Флоріса про те, що при вилученні 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є у двох бітах, що передують c(і під час переміщення його натискають на чуже місце c):

  a000b0c0
+ 0b0c0000
+ b0c00000
= Xbc.....

...a...bc...d... Працює, оскільки послідовні біти зміщуються разом:

  a000bc000d
+ 0bc000d000
+ 000d000000
= abcd000000

Але у нас є проблема.Якщо ми використовуємо n-iзамість цього, у n-1нас може бути наступний сценарій: що робити, якщо у нас зіткнення поза частиною, про яку ми піклуємося, щось, що ми замаскували б у кінці кінців, але чиї біт-носи в кінцевому підсумку втручаються у важливий безмаскований діапазон ? (і зауважте: n-1вимога гарантує, що цього не відбувається, переконуючись, що i-1біти після нашого немаскованого діапазону є чіткими, коли ми зміщуємо ith біт)

...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з очікуваним результатом «всі» -1 << (64-n)(для 64-розрядних неподписаних)

Чарівний зсув / множення для вилучення наших шматочків спрацьовує, якщо і тільки якщо два рівні.


Мені це подобається - ви праві, що на кожен біт вам потрібно лише стільки нулів праворуч від нього, скільки вам потрібно місця для біт, які потрібно туди перейти. Але також йому потрібно стільки ж бітів ліворуч, скільки має біт результатів ліворуч. Так що, якщо трохи bзакінчується в положенні mпро n, то вона повинна мати m-1нулі зліва від нього , і n-m-1нулі справа від нього . Особливо, коли біти не в такому ж порядку в початковій кількості, як вони будуть після повторного замовлення, це важливе поліпшення вихідних критеріїв. Це весело.
Флоріс

13

Окрім вже чудових відповідей на це дуже цікаве запитання, може бути корисним знати, що цей трюковий множник розмноження відомий у шаховій спільноті комп'ютерів з 2007 року, де він під назвою Magic BitBoards .

Багато шахматних комп'ютерних двигунів використовують декілька 64-бітних цілих чисел (званих бітбордами) для представлення різних наборів штук (1 біт на зайнятий квадрат). Припустимо, ковзний шматок (грак, єпископ, королева) на певній площі походження може переміститися в максимум Kквадрати, якщо не було блокуючих фігур. Використання порозрядних і розсіяних Kбітів з бітбордом зайнятих квадратів дає конкретне K-бітове слово, вбудоване в 64-бітове ціле число.

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

Типовий шаховий двигун, що використовує цей підхід, має 2 таблиці (одна для граків, друга для єпископів, королеви, що використовують комбінацію обох) із 64 записів (по одному на квадрат походження), які містять такі попередньо обчислені результати. Як шаховий двигун із найвищим рейтингом ( Худіні ), так і шаховий двигун із відкритим кодом ( Stockfish ) в даний час використовують цей підхід для його дуже високої продуктивності.

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

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


Я б подумав, що кожен, хто має повноцінний офіційний досвід роботи з CS, стрибнув би на підхід SAT безпосередньо, побачивши цю проблему. Можливо, людям ЦС шахи вважають нецікавими? :(
Відновіть Моніку

@KubaOber Це здебільшого навпаки: в комп'ютерних шахах переважають біт-супінатори, які програмують на C або збірку, і ненавидять будь-яку абстракцію (C ++, шаблони, ОО). Я думаю, що це відлякує справжніх хлопців CS :-)
TemplateRex
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.