使用位域或位运算符在字节内移动位

5

在一个字节(或字/长字)中,有没有一种优雅的方法可以移动一位。为简单起见,让我们使用一个简单的8位字节和仅一个位来移动字节内的内容。

给定一个位数,基于0-7最低有效位到最高有效位(或者如果你愿意,是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。 - 可能从字节扩展到unsigned int、long等。 - (理想情况下,可扩展到一次移动多个位,如果更容易,可能是相邻的位)
性能不是主要问题,但是优雅的解决方案很可能足够快。
我的天真方法是识别源和目标位位置,决定向上或向下移动,取一个移动后的副本,屏蔽静态位并找到源位,合并静态和移动位,并以某种方式设置/清除目标位。然而,虽然理论看起来不错,但优雅的实现超出了我的能力。
我意识到可以为字节构建预编译的查找表,但如果要将其扩展到整数/长整数,则对我来说不切实际。
任何帮助都将不胜感激。提前致谢。

就这种操作(位域操作)而言,PowerPC ISA有一些非常好的指令。否则,在高级语言中使用移位和掩码操作也不难实现。 - Paul R
我知道看起来并不难,但在实践中却要困难得多/不够优雅。如果HLL代表高级语言,即使如此,除了位域和布尔数组之外,我仍然没有一个好的解决方案。 - andora
1
位操作很少优雅。使其优雅通常意味着将其隐藏在函数(或可能是宏)后面。 - Chris Lutz
@chris:嗯,也许吧,这取决于语言提供的支持。在我的应用程序中,我不会使用C或类似的语言,因此可以在汇编语言或基本位运算中完成的内容是一个优势。位域/数组/结构等都可以,但对我来说了解底层的情况很好。Mathews的答案看起来非常好(即:优雅),并且完全适合我。 - andora
4个回答

6

首先,关于原始问题和你提到的后续扩展的观察:

你描述的“移动一点”的操作实际上是一个连续比特范围的旋转。在你的示例中,你将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

...可以通过以下方式来创建:

00000001

将5+1=6位左移:

01000000

...并减去1,得到:

00111111

在 C 语言中,这个表达式是 (1 << (bit + 1)) - 1。但是这里有一个微妙的问题,至少在 C 语言中存在(尽管你将其标记为与语言无关,我很抱歉会打岔,但这很重要,其他语言可能也存在类似的问题):如果移位的位数等于或超过类型的宽度,则导致未定义的行为。因此,如果我们试图为 8 位类型的位 0-7 构建掩码,则计算结果将为 (1 << 8) - 1,这将是未定义的。(它可能在某些系统和编译器上有效,但不具备可移植性)。对于带符号类型,在移位到符号位时也存在未定义的行为问题。
幸运的是,在 C 语言中,我们可以通过使用无符号类型,并将表达式写成 (1 << bit) + (1 << bit) - 1 来避免这些问题。标准规定无符号 n 位值的算术运算应该被模 2n 缩小,所有单独的操作都是明确定义的,因此我们保证得到正确的答案。
现在,我们有了位 0-msb 的掩码。我们想要制作位 lsb-msb 的掩码,可以通过减去位 0-(lsb-1)的掩码来实现,即 (1 << 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);

可以通过与掩码进行按位 AND 运算来选择要旋转的位:

to_rotate = value & mask;

未被修改的位可以通过与反掩码进行AND选择:

untouched = value & ~mask;

旋转本身可以轻松地分为两部分完成:首先,我们可以通过将to_rotate左移并且丢弃掉超出掩码范围的任何位来获取旋转部分的最左边的位:
left = (to_rotate << shift) & mask;

为了获得最右边的位,将 to_rotate旋转 (n - shift) 个比特位,其中 n 是我们要旋转的比特数(这个 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

1
Mathew:多好的回答和例子啊。感谢您抽出时间详细说明过程。可惜我只能给您一个赞,但我很高兴接受这个答案。(只有一个小问题,在16位示例中,最后一阶段显示未更改,但在pos14处设置了一个应该被清除的位) - andora
不客气;感谢你发现了这个错误 - 我已经修复了! - Matthew Slattery

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 位旁边(如果向下移位),只需要左移吗? - andora
它并不是完全优化的 - extract_bit将会在需要时将(某些)LS位向上移动一位,而insert_bit将会在需要时将(某些)LS位向下移动一位。这可能看起来效率低下,毫无疑问,你可以将其压缩成一个单一的例程,但我怀疑你需要额外的逻辑来处理边缘情况,并且在某些情况下仍然需要两个移位。 - Paul R
谢谢Paul。我看到你首先提供了Mathews回答中涵盖的许多过程细节,但我认为他总体上给出了更好的答案,包括移动超过1位的方法。通过点赞表达感激之情。 - andora

1

这可能不太优雅,但如果您喜欢这种方式,您可能可以将其压缩成一行?计划是将数字分成四个部分(使用位运算应该不难,对吧?),对它们进行适当的操作,然后将三个部分重新组合在一起。

              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向上移动1位] + [P2向下移动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。 - andora
如果你将 something AND 0011 1111 1111 想象成截掉一个数字的末尾,将 something MOD 0100 0000 0000 看作相反的操作,使用 AND 和 MOD,你可以制作出 P1、P2、P3 和 P4... 它是有效的。但这并不是“优雅”的解决方案,我也想要一个优雅的答案,这就是为什么我赞同你的问题的原因。 :) - Chris Cunningham
我也在思考类似的想法,你可以通过起始/结束位的异或运算来得出P3,然后使用P3作为掩码对移位后的P3部分进行操作。有趣!+1 谢谢!! - andora

0
你是否使用位来节省空间?这真的有必要吗?
也许你最好使用一个列表类,它允许你在列表中删除和插入项目。在你的情况下,这些项目将是布尔值。

不,这与空间限制无关,而是真的想要(!)一般的位操作,可能适用于编码。我想我可以解析成一个布尔数组,重新排列并混合回一个字节,但我觉得这不是很优雅。谢谢你的答案。 - andora

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接