【问题标题】:Moving a bit within a byte using bitfield or bitwise operators使用位域或位运算符在字节内移动位
【发布时间】:2011-06-21 19:44:42
【问题描述】:

是否有一种优雅的方式在一个字节(或字/长)内移动位。为简单起见,让我们使用一个简单的 8 位字节,并在字节内移动一位。

给定一个位数,基于 0-7 Least-sig-bit 到 most-sig-bit,(如果您愿意,也可以是 1-8 位),我想从一个位置移动一点到另一个位置:

7654 3210 <bit position
0101 1010 <some binary value
--x- --y- <move bit from x to y
0111 0100 <new value with x moved to y and intervening bits shifted left

因此,第 5 位的 x 移动到第 1 位的 y,第 0、6、7 位保持不变。位 2、3、4 向左移动,以便为从 5 移动到 2 的位“腾出空间”。这只是一个示例。

重要的是位移动,而不是与其目标交换。有许多位交换的示例,但这是非常微不足道的。

理想的解决方案是使用简单的位旋转和位运算符。假设语言不可知,简单的 AND/OR/XOR、NOT、SHIFT Left/Right/ROTATE 或类似指令在任何组合中都可以,再加上任何其他基本算术运算符,例如:mod、加法/减法等。即使是伪工作代码就可以了。或者,位数组或位域类型结构可能很简单。

除了实际的位移动,我想找到一种方法:

  • 向上或向下移动任何位。
  • 以任何方便的格式指定位号源/目标:例如:6>2 意味着向下移位,3>7 向上移位或起始位 +/- 偏移:6-4 或 3+4,或位加权:位 6=64 到位 3=8。
  • 可能从字节扩展到无符号整数、长整数等。
  • (理想情况下,可扩展 一次超过一个位,如果更容易,可能是相邻位)

性能不是主要问题,但优雅的东西可能足够快。

我自己的幼稚方法是识别源位和目标位位置,决定是上移还是下移,获取移位副本,屏蔽静态位并找到源位,合并静态位和移位位并以某种方式设置/清除目标位。然而,虽然理论看起来不错,但优雅的实现却超出了我的想象。

我意识到可以为一个字节构建一个预编译的查找表,但是如果要将其扩展到整数/长整数,这对我来说是不切实际的。

任何帮助表示赞赏。提前致谢。

【问题讨论】:

  • FWIW PowerPC ISA 有一些很好的指令来做这种事情(位域操作)。否则,在 HLL 中使用 shift 和 mask 操作并不难。
  • 我知道这看起来并不难,但在实践中实际做起来却很难/不优雅。如果 HLL=High-Level-Lang,那么即便如此,除了位域和布尔数组,我仍然没有一个好的解决方案。
  • Bit twiddling 很少是优雅的。使其优雅通常意味着将其隐藏在函数(或可能是宏)后面。
  • @chris:嗯,也许,这取决于语言提供的支持。在我的应用程序中,我不会使用 C 或类似语言,因此可以在 Assembler 或基本按位中完成的事情是一个奖励。位域/数组/结构等都可以,但很高兴知道(无论如何对我来说)引擎盖下发生了什么。 Mathews 的答案对我来说看起来不错(即:优雅),非常适合我。

标签: language-agnostic bit-manipulation puzzle bit


【解决方案1】:

首先,对原始问题的观察,以及您提到的后续扩展:

您描述的“移动一点”操作实际上是连续位范围的旋转。在您的示例中,您将 1-5 位(含)向左旋转一位:

  7   6   5   4   3   2   1   0          7   6   5   4   3   2   1   0
+---+---+---+---+---+---+---+---+      +---+---+---+---+---+---+---+---+
| 0 | 1 | 0<--1<--1<--0<--1 | 0 |  ->  | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 0 |
+---+---+-|-+---+---+---+-^-+---+      +---+---+---+---+---+---+---+---+
          |               |
          +---------------+

如果您认为此操作的更一般形式是使用三个参数“将某个范围内的位旋转一定量”:

  1. 要包含在轮换中的最低有效位
  2. 要包含在轮换中的最高有效位
  3. 要循环的位数

然后它变成一个单一的基本原语,可以执行所有你想做的事情:

  • 您显然可以移动任何位(选择适当的最低/最高有效位参数);
  • 您可以向左或向右旋转,因为如果您要旋转 n 位范围,则向右旋转 k 位与向左旋转相同按 n - k 位;
  • 它可以简单地推广到任何位宽;
  • 根据定义,我们一次可以旋转不止一位。

所以现在,所需要的就是构建这个原语......


首先,我们几乎肯定需要一个位掩码来处理我们关心的位。

我们可以通过将 1 向左移动 n + 1 位,然后减去 1 来形成位 0 - n 的掩码。例如位 0-5 的掩码将是(二进制):

00111111

...可以通过取1来形成:

00000001

...向左移动 5+1 = 6 位:

01000000

...减去 1 得到:

00111111

在 C 中,这将是 (1 &lt;&lt; (bit + 1)) - 1。但是这里有一个微妙之处,至少对于 C 而言(当您将其标记为与语言无关时,我为题外话道歉,但这很重要,并且在其他语言中也可能存在类似问题):您的类型(或更多)的宽度会导致未定义的行为。因此,如果我们尝试为 8 位类型的 0-7 位构建掩码,则计算结果将是 (1 &lt;&lt; 8) - 1,这将是未定义的。 (它可能适用于某些系统和某些编译器,但不能移植。)在您最终会转移到符号位的情况下,有符号类型也存在未定义的行为问题。

幸运的是,在 C 语言中,我们可以通过使用 unsigned 类型并将表达式编写为 (1 &lt;&lt; bit) + (1 &lt;&lt; bit) - 1 来避免这些问题。具有无符号 n 位值的算术由标准定义为模 2n 减少,并且所有单独的操作都是明确定义的,因此我们保证得到正确的答案。

(题外话结束。)

好的,所以现在我们有了位 0 - msb 的掩码。我们想为位 lsb - msb 做一个掩码,我们可以通过减去位 0 - (lsb-1) 的掩码来实现,即(1 &lt;&lt; lsb) - 1。例如

  00111111      mask for bits 0-5:  (1 << 5) + (1 << 5) - 1
- 00000001      mask for bits 0-0:  (1 << 1) - 1
  --------                         -------------------------------
  00111110      mask for bits 1-5:  (1 << 5) + (1 << 5) - (1 << 1)

所以掩码的最终表达式是:

mask = (1 << msb) + (1 << msb) - (1 << lsb);

可以通过按位与掩码选择要旋转的位:

to_rotate = value & mask;

...可以通过带有反转掩码的 AND 选择将保持不变的位:

untouched = value & ~mask;

旋转本身可以很容易地分两部分进行:首先,我们可以通过简单地向左旋转to_rotate并丢弃任何落在掩码之外的位来获得旋转部分的最左边位:

left = (to_rotate << shift) & mask;

要获得最右边的位,请将to_rotate right 旋转 (n - shift) 位,其中 n em> 是我们正在旋转的位数(这个 n 可以计算为msb + 1 - lsb):

right = (to_rotate >> (msb + 1 - lsb - shift)) & mask;

untouchedleftright中的所有位组合起来即可得到最终结果:

result = untouched | left | right;

您的原始示例将像这样工作(msb 是 5,lsb 是 1,shift 是 1):

    value = 01011010

    mask  = 00111110   from (1 << 5) + (1 << 5) - (1 << 1)

            01011010   value
          & 00111110   mask
          ----------
to_rotate = 00011010

            01011010   value
          & 11000001   ~mask  (i.e. inverted mask)
          ----------
untouched = 01000000

            00110100   to_rotate << 1
          & 00111110   mask
          ----------
     left = 00110100

            00000001   to_rotate >> 4  (5 + 1 - 1 - 1 = 4)
          & 00111110   mask
          ----------
    right = 00000000

            01000000   untouched
            00110100   left
          | 00000000   right
          ----------
   result = 01110100

下面是一个不同的示例,它具有 16 位输入值,msb = 15、lsb = 4 和 shift = 4(旋转 4 位十六进制值的前 3 个十六进制数字):

    value = 0101011001111000   (0x5678)

    mask  = 1111111111110000   from (1 << 15) + (1 << 15) - (1 << 4)

            0101011001111000   value
          & 1111111111110000   mask
          ------------------
to_rotate = 0101011001110000

            0101011001111000   value
          & 0000000000001111   ~mask
          ------------------
untouched = 0000000000001000

            0110011100000000   to_rotate << 4
          & 1111111111110000   mask
          ------------------
     left = 0110011100000000

            0000000001010110   to_rotate >> 8  (15 + 1 - 4 - 4 = 8)
          & 1111111111110000   mask
          ------------------
    right = 0000000001010000

            0000000000001000   untouched
            0110011100000000   left
          | 0000000001010000   right
          ------------------
   result = 0110011101011000   =  0x6758

【讨论】:

  • Mathew:多么棒的答案和例子啊。感谢您抽出宝贵时间详细说明该过程。很遗憾我只能给你 1 票,但我很高兴接受这个作为答案。 (只是一个小的挑剔,在 16 位示例中,最后阶段显示未触及,设置在 pos14 的位应该是清晰的)
  • 不客气;并感谢您发现错误 - 我现在已经修复了!
【解决方案2】:

这是一个 C 语言的工作实现,它没有经过高度优化,但至少可以作为任何进一步实现的起点。它适用于整数,但您可以将其调整为任何字长,或者直接使用它按原样并屏蔽掉任何不需要的高位(例如,如果您正在处理单个字节)。我将功能分解为两个较低级别的例程,用于提取位和插入位 - 我想这些可能还有其他用途。

//
// bits.c
//

#include <stdio.h>
#include <stdlib.h>

//
// extract_bit
//
// extract bit at given index and move less significant bits left
//

int extract_bit(int *word, int index)
{
    int result = (*word & (1 << index)) != 0;
    int mask = (1 << index) + (1 << index) - 1;
    *word = ((*word << 1) & mask) | (*word & ~mask);
    return result;
}

//
// insert_bit
//
// insert bit at given index and move less significant bits right
//

void insert_bit(int *word, int index, int val)
{
    int mask1 = (1 << index) + (1 << index) - 1;
    int mask2 = (1 << index) - 1;
    *word = ((*word >> 1) & mask2) | (*word & ~mask1) | (val << index);
}

//
// move_bit
//
// move bit from given src index to given dest index
//

int move_bit(int *word, int src_index, int dest_index)
{
    int val = extract_bit(word, src_index);
    insert_bit(word, dest_index, val);
    return val;
}

int main(int argc, char * argv[])
{
    if (argc > 2)
    {
        int test = 0x55555555;
        int index1 = atoi(argv[1]);
        int index2 = atoi(argv[2]);

        printf("test (before) = %#x\n", test);
        printf("index (src) = %d\n", index1);
        printf("index (dest) = %d\n", index2);

        move_bit(&test, index1, index2);

        printf("test (after) = %#x\n", test);
    }

    return 0;
}

【讨论】:

  • 有趣 - 谢谢。需要详细了解这一点,但只是想知道是否需要右移?我认为 LSB 将是静态的,移动位应该移动到 LSB 位旁边(如果向下移位)并且只需要向左移位?
  • 它并没有真正完全优化——如果需要,extract_bit 会将(一些)LS 位向上移动一位,并且如果需要,insert_bit 会再次向下移动(一些)LS 位。它可能看起来效率低下,毫无疑问您可以将其浓缩为一个例程,但我怀疑您需要额外的逻辑来处理边缘情况,并且在某些情况下可能仍然需要两个班次。
  • 谢谢保罗。我看到您首先给出了 Mathews 答案中涵盖的许多过程细节,但我认为他总体上给出了更好的答案,包括移位超过 1 位的方式。以赞赏的方式投票。
【解决方案3】:

这可能不符合“优雅”的条件,但如果你喜欢这样的话,你也许可以把它塞进一行?计划是将数字分成四部分(用位操作应该不难吧?),对它们做适当的事情,然后将三部分重新组合在一起。

              Number: 01x1 10y1
       P1 (before x): 0100 0000
     P2 (just bit x): 00x0 0000
P3 (between x and y): 0001 10y0
        P4 (after y): 0000 0001

那么你要的号码是[P1] + [P3 shifted up by 1] + [P2 shifted down by 4] + [P4]

                  P1: 0100 0000
P2 shifted down by 3: 0000 00x0
  P3 shifted up by 1: 0011 0y00
                  P4: 0000 0001

                 Sum: 0111 0yx1               

【讨论】:

  • 为澄清起见,x 移动以替换位置 y 处的位,因此 P3 包含 y 并且 y 位向上/向左移动以移开。我明白你的意思,“棘手的部分”是以选择 P1、P2、P3 和 P4 的方式指定位数/位置。
  • 如果您将something AND 0011 1111 1111 视为截断数字的末尾,而将something MOD 0100 0000 0000 视为相反,您可以使用AND 和MOD 生成P1、P2、P3 和P4。 。 有用。但这不是“优雅的”,我也想要一个优雅的答案,这就是为什么我赞成你的问题。 :)
  • 我在考虑类似的思路,您可以从开始/结束位的 XOR 导出 P3,然后使用 P3 作为对移位 P3 部分的掩码。有趣的 +1 谢谢!!
【解决方案4】:

您是否使用位来节省空间?真的需要吗?

使用允许您在列表中删除和插入项目的列表类可能会更好。在您的情况下,这些项目将是布尔值。

【讨论】:

  • 不,与空间限制无关,是的,真的想要(!)可能适用于编码的一般位操作。认为我可以解析为一个布尔数组,重新排列并混合回一个字节,但并没有让我觉得非常优雅。谢谢你的回答。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-05
  • 1970-01-01
相关资源
最近更新 更多