使用单个乘法提取位

时间:2013-01-27 11:58:24

标签: c multiplication bit-manipulation

我在answeranother question中看到了一种有趣的技巧,并且想要更好地理解它。

我们给出了一个无符号的64位整数,我们对以下几位感兴趣:

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

具体来说,我们希望将它们移到前八位,如下所示:

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

我们不关心.指示的位值,并且不必保留它们。

solution用于屏蔽不需要的位,并将结果乘以0x2040810204081。事实证明,这就是诀窍。

这种方法有多普遍?这种技术可以用来提取任何比特子集吗?如果不是,那么如何判断该方法是否适用于特定的一组位?

最后,如何找到(a?)正确的乘数来提取给定的比特?

5 个答案:

答案 0 :(得分:231)

非常有趣的问题和聪明的伎俩。

让我们看一个操作单个字节的简单示例。使用无符号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......

通过这种方法,您可以扩展到更大的数字和更多的位。

你提出的一个问题是#34;这可以用任意数量的位完成吗?"我认为答案是" no",除非您允许多次屏蔽操作或多次乘法。问题是"碰撞" - 例如,"流浪b"在上面的问题。想象一下,我们需要对xaxxbxxcx之类的数字执行此操作。按照早期的方法,你会认为我们需要{x 2,x {1 + 4 + 16}} = x 42(噢 - 一切的答案!)。结果:

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

正如您所看到的,它仍然有效,但仅仅是"。他们关键的是有足够的空间和#34;在我们想要的位之间,我们可以挤压一切。我无法在c之后添加第四位d,因为我会得到我得到c + d的实例,可能带有位,......

因此,如果没有正式的证据,我会回答你问题中更有趣的部分如下:"不,这对任意数量的位都不起作用。要提取N位,您需要在要提取的位之间留出(N-1)个空格,或者有额外的掩码乘法步骤。"

我能想到的唯一例外是"在比特之间必须有(N-1)个零"规则是这样的:如果你想提取原始中彼此相邻的两个位,并且你希望它们保持相同的顺序,那么你仍然可以这样做。并且出于(N-1)规则的目的,它们被计为两位。

还有另一种见解 - 受以下@Ternary答案的启发(见我在那里的评论)。对于每个有趣的位,您只需要在其右侧有多个零,因为您需要空间来存在需要的位。但是,它左边需要尽可能多的位,因为左边有结果位。因此,如果位b在n的位置m结束,则它需要在其左侧具有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)规则:&#34;如果位之间至少有(N-1)个空格,它将始终有效;或者,如果最终结果中的比特顺序是已知的,那么如果比特b在n的位置m结束,则其左边需要m-1个零,并且右边的n为零。&#34;

@Ternary指出,这个规则并不是很有效,因为可以通过位添加&#34;只是在目标区域的右侧&#34; - 即,当我们寻找的位都是1时。继续我在上面给出了一个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)规则。如果有多个位具有&#34;正好(nm)未使用的位(不计算模式中的最后一位 - &#34; c&#34;在上例中),那么我们需要加强规则 - 我们必须反复这样做!

我们不仅需要查看符合(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,那么您需要将RHS标准增加一位到(n-m+1)。此时,只要W < 4,操作就是安全的;如果这不起作用,再增加一个标准,等等。

我认为按照上述步骤可以帮助您解决问题......

答案 1 :(得分:151)

确实非常有趣的问题。我正在用我的两分钱,这就是说,如果你可以通过比特向量理论的一阶逻辑设法说明这样的问题,那么定理证明是你的朋友,并且可以为你提供非常快的速度回答你的问题。让我们重新陈述被问为一个定理的问题:

“存在一些64位常量'掩码'和'被乘数',这样,对于所有64位位向量x,在表达式y =(x&amp; mask)*被乘数中,我们得到y.63 = = x.63,y.62 == x.55,y.61 == x.47等。“

如果这个句子实际上是一个定理,那么常量'mask'和'multiplicand'的某些值确实满足这个属性。所以让我们用一个定理证明者可以理解的东西来表达这个,即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 :(得分:85)

乘法器中的每1位用于将其中一个位复制到正确的位置:

  • 1已经处于正确位置,因此乘以0x0000000000000001
  • 2必须向左移动7位,因此我们乘以0x0000000000000080(设置第7位)。
  • 3必须向左移动14位,因此我们乘以0x0000000000000400(设置位14)。
  • 等等,直到
  • 8必须向左移动49位,因此我们乘以0x0002000000000000(设置位49)。

乘数是各个位的乘数之和。

这只能起作用,因为要收集的位不是太靠近,因此在我们的方案中不属于一起的位的乘法要么超出64位,要么落在较低的无关注部分。 / p>

请注意,原始号码中的其他位必须为0。这可以通过使用AND操作屏蔽它们来实现。

答案 3 :(得分:29)

(我之前从未见过。这个技巧很棒!)

我将对Floris的断言进行一些扩展,即在提取n位时,任何非连续位之间需要n-1空格:

我最初的想法(我们会在一分钟内看到这不起作用)是你可以做得更好:如果你想提取n位,你提取时会发生碰撞/如果您之前的i位或之后的i位中有任何人(非连续的位i-1),则移位位n-i

我将举几个例子来说明:

...a..b...c...有效(a之后的2位,前一位和b之后的位,没有人在c之前的2位中:

  a00b000c
+ 0b000c00
+ 00c00000
= abc.....

...a.b....c...失败,因为b位于a之后的2位(当我们转移a时会被拉入别人的位置):

  a0b0000c
+ 0b0000c0
+ 00c00000
= abX.....

...a...b.c...失败,因为b位于c之前的2位(当我们转移c时会被推入别人的位置):

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

...a...bc...d...可以工作,因为连续的位一起移动:

  a000bc000d
+ 0bc000d000
+ 000d000000
= abcd000000

但我们遇到了问题。如果我们使用n-i代替n-1,我们可能会遇到以下情况:如果我们在部件之外发生碰撞会怎么样?关心,最后我们会屏蔽掉的东西,但是其携带位最终会干扰重要的未屏蔽范围? (并注意:n-1要求确保在我们移位i-1位时确保未屏蔽范围之后的i位清零时不会发生这种情况。 / p>

...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位无符号)

当且仅当两者相等时,魔术移位/乘法才能提取我们的位。

答案 4 :(得分:12)

除了对这个非常有趣的问题已经很好的答案之外,知道自2007年以来计算机国际象棋界已经知道这种按位乘法技巧可能是有用的,其名称为Magic BitBoards

许多计算机国际象棋引擎使用多个64位整数(称为位板)来表示各种棋子组(每个占用方块1位)。假设如果没有阻挡件,某个原点上的滑块(车,主教,女王)可以移动到最多K个方块。使用bitwise-和那些分散的K位和占用方块的位板,可以在64位整数中嵌入一个特定的K位字。

Magic multiplication可用于将这些分散的K位映射到64位整数的低K位。然后可以使用这些较低的K位来索引预先计算的位板表,该位表示其原点上的块可以实际移动到的允许的方块(处理阻塞件等)

使用这种方法的典型国际象棋引擎有两个表(一个用于车,一个用于主教,两个使用两者的组合),64个条目(每个原点方格一个)包含这样的预先计算结果。最高评级的闭源(Houdini)和开源象棋引擎(Stockfish)目前都使用这种方法,因为它具有非常高的性能。

使用exhaustive search(使用早期截止优化)或使用trial and erorr(例如,尝试大量随机64位整数)来查找这些魔术乘数。在移动生成期间没有使用位模式,其中没有找到魔法常数。然而,当待映射比特具有(几乎)相邻索引时,通常需要逐位进位效应。

AFAIK是@Syzygy非常普遍的SAT求解器,并没有被用于计算机国际象棋,也似乎没有关于这种魔法常数的存在性和唯一性的任何形式理论。