【问题标题】:Bit duplication from 8-bit to 32-bit从 8 位到 32 位的位复制
【发布时间】:2020-12-08 17:34:30
【问题描述】:

我正在尝试将 8 位值复制到 32 位,并想问是否可以编写单行算法来复制位值。

例如:

1100 1011 -> 1111 1111 0000 0000 1111 0000 1111 1111

如果可能的话,我想了解其背后的逻辑。

【问题讨论】:

  • 换句话说,您的目标是将 8 位字节中的每一位转换为 O(1) 中的 nybble?
  • 答案是肯定的。该解决方案背后的逻辑很简单,就是您不要在其中添加换行符。
  • _pdep_u32 可用吗?
  • 你能依赖任何处理器架构(例如 x86)或指令集(例如 BMI2)吗?
  • 我正在尝试将其写在 PIC18 微芯片 (PIC18F46J50) 上。 @harold 它不可用

标签: c duplicates bit-manipulation expansion


【解决方案1】:

这很简单 - 解决最简单的情况,然后做更复杂的情况。

案例 1:将 1 位复制为 4 位值(最简单)。

+---+---------+
| 0 | _ _ _ A |
+---+---------+
| 1 | A A A A |
+---+---------+

这可以通过一组简单的班次来完成:

x = (x << 0) | (x << 1) | (x << 2) | (x << 3);

或者以一种不太明显但更快的方式:

x = (x << 4) - x;

这一步将是以下所有情况的最后一步。

案例 2:将 2 位复制为 8 位值。

+---+---------+---------+
| 0 | _ _ _ _ | _ _ A B |
+---+---------+---------+
| 1 | _ _ _ A | _ _ _ B |
+---+---------+---------+
| 2 | A A A A | B B B B |
+---+---------+---------+

案例 3:将 4 位复制为 16 位值。如何?只需将 2 位移动到上部即可将其变成案例 1!分而治之!

+---+---------+---------+---------+---------+
| 0 | _ _ _ _ | _ _ _ _ | _ _ _ _ | A B C D |
+---+---------+---------+---------+---------+
| 1 | _ _ _ _ | _ _ A B | _ _ _ _ | _ _ C D |
+---+---------+---------+---------+---------+
| 2 | _ _ _ A | _ _ _ B | _ _ _ C | _ _ _ D |
+---+---------+---------+---------+---------+
| 3 | A A A A | B B B B | C C C C | D D D D |
+---+---------+---------+---------+---------+

案例 4:将 8 位复制为 32 位值(原始值)。

+---+---------+---------+---------+---------+---------+---------+---------+---------+
| 0 | _ _ _ _ | _ _ _ _ | _ _ _ _ | _ _ _ _ | _ _ _ _ | _ _ _ _ | A B C D | E F G H |
+---+---------+---------+---------+---------+---------+---------+---------+---------+
| 1 | _ _ _ _ | _ _ _ _ | _ _ _ _ | A B C D | _ _ _ _ | _ _ _ _ | _ _ _ _ | E F G H |
+---+---------+---------+---------+---------+---------+---------+---------+---------+
| 2 | _ _ _ _ | _ _ A B | _ _ _ _ | _ _ C D | _ _ _ _ | _ _ E F | _ _ _ _ | _ _ G H |
+---+---------+---------+---------+---------+---------+---------+---------+---------+
| 3 | _ _ _ A | _ _ _ B | _ _ _ C | _ _ _ D | _ _ _ E | _ _ _ F | _ _ _ G | _ _ _ H |
+---+---------+---------+---------+---------+---------+---------+---------+---------+
| 4 | A A A A | B B B B | C C C C | D D D D | E E E E | F F F F | G G G G | H H H H |
+---+---------+---------+---------+---------+---------+---------+---------+---------+

可以通过以下代码实现:

uint32_t interleave(uint8_t value)
{
    uint32_t x = value;
    x = (x | (x << 12)) /* & 0x000F000F */; // GCC is not able to remove redundant & here
    x = (x | (x <<  6)) & 0x03030303;
    x = (x | (x <<  3)) & 0x11111111;
    x = (x << 4) - x;
    return x;
}

一些测试用例来检查它是否有效:

TEST_F(test, interleave)
{
    EXPECT_EQ(interleave(0x00), 0x00000000);
    EXPECT_EQ(interleave(0x11), 0x000F000F);
    EXPECT_EQ(interleave(0x22), 0x00F000F0);
    EXPECT_EQ(interleave(0x33), 0x00FF00FF);
    EXPECT_EQ(interleave(0x44), 0x0F000F00);
    EXPECT_EQ(interleave(0x55), 0x0F0F0F0F);
    EXPECT_EQ(interleave(0x66), 0x0FF00FF0);
    EXPECT_EQ(interleave(0x77), 0x0FFF0FFF);
    EXPECT_EQ(interleave(0x88), 0xF000F000);
    EXPECT_EQ(interleave(0x99), 0xF00FF00F);
    EXPECT_EQ(interleave(0xAA), 0xF0F0F0F0);
    EXPECT_EQ(interleave(0xBB), 0xF0FFF0FF);
    EXPECT_EQ(interleave(0xCC), 0xFF00FF00);
    EXPECT_EQ(interleave(0xDD), 0xFF0FFF0F);
    EXPECT_EQ(interleave(0xEE), 0xFFF0FFF0);
    EXPECT_EQ(interleave(0xFF), 0xFFFFFFFF);

    EXPECT_EQ(interleave(0x01), 0x0000000F);
    EXPECT_EQ(interleave(0x23), 0x00F000FF);
    EXPECT_EQ(interleave(0x45), 0x0F000F0F);
    EXPECT_EQ(interleave(0x67), 0x0FF00FFF);
    EXPECT_EQ(interleave(0x89), 0xF000F00F);
    EXPECT_EQ(interleave(0xAB), 0xF0F0F0FF);
    EXPECT_EQ(interleave(0xCD), 0xFF00FF0F);
    EXPECT_EQ(interleave(0xEF), 0xFFF0FFFF);
}

【讨论】:

  • 改进了最后一步(受@njuffa 回答的启发)。
  • 聪明的代码!看来您可以进一步简化最后一步:x = (x &lt;&lt; 4) - x;
  • @StaceyGirl 当为 Pascal 系列 GPU 编译时,改进的版本归结为只有 12 条指令,因为编译器能够在两个地方使用乘加:LOP32I.AND R0, R4, 0xff; SHL R3, R0, 0xc; LOP.OR R0, R3, R0; LOP32I.AND R3, R0, 0xc000c; SHL R3, R3, 0x6; LOP3.LUT R0, R3, 0x30003, R0, 0xf8; SHL R3, R0, 0x3; LOP3.LUT R0, R3, c[0x0][0x0], R0, 0xc8; XMAD R5, R0.reuse, 0x7, RZ; SHL R3, R0.reuse, 0x3; XMAD.PSL R0, R0.H1, 0x7, R5; LOP.OR R4, R0, R3;
  • @chqrlie 该修改将代码减少到 Pascal 系列 GPU 上的 10 条指令:LOP32I.AND R0, R4, 0xff; SHL R3, R0, 0xc; LOP.OR R0, R3, R0; LOP32I.AND R3, R0, 0xc000c; SHL R3, R3, 0x6; LOP3.LUT R0, R3, 0x30003, R0, 0xf8; SHL R3, R0, 0x3; LOP3.LUT R0, R3, c[0x0][0x0], R0, 0xc8; XMAD R3, R0.reuse, 0xf, RZ; XMAD.PSL R4, R0.H1, 0xf, R3;
  • @StaceyGirl:经过更多分析,x = (x | (x &lt;&lt; 12)) &amp; 0x000F000F; 中的掩码似乎也是多余的。 x |= x &lt;&lt; 12; 应该足够了。第二个面具也可能是多余的,但我还不确定。
【解决方案2】:

只有 256 个 8 位值,所以一个简单的查找表会占用 1kb,而且查找是微不足道的。很难相信任何 bithack 都会有出色的性能。

【讨论】:

  • 那将是一条该死的长单行:D
  • 明智的方式(第二次正确阅读问题)。
  • 或者可以简化为一个包含 16 个条目的查找表,并逐字节工作。当然会有更多的“代码”。
  • @eugenesh:由于目标设备似乎只有 8 位数据路径并且没有任何形式的 32 位寄存器,我猜想使用 2 位索引的 4 条目 LUT 可能是合适的。
【解决方案3】:

这可行:

unsigned int eToTW (unsigned char a) {
    unsigned int output = 0;

    output |= a & 0x80 ? ((unsigned) 0xf) << 28 : 0x0;
    output |= a & 0x40 ? 0xf << 24 : 0x0;
    output |= a & 0x20 ? 0xf << 20 : 0x0;
    output |= a & 0x10 ? 0xf << 16 : 0x0;

    output |= a & 0x8 ? 0xf << 12 : 0x0;
    output |= a & 0x4 ? 0xf << 8 : 0x0;
    output |= a & 0x2 ? 0xf << 4 : 0x0;
    output |= a & 0x1 ? 0xf : 0x0;

    return output;
}

或者这个:

unsigned int eToTW (unsigned char a) {
    unsigned int output = 0;

    output |= a & (1 << 7) ? ((unsigned) 0xf) << 28 : 0x0;
    output |= a & (1 << 6) ? 0xf << 24 : 0x0;
    output |= a & (1 << 5) ? 0xf << 20 : 0x0;
    output |= a & (1 << 4) ? 0xf << 16 : 0x0;

    output |= a & (1 << 3) ? 0xf << 12 : 0x0;
    output |= a & (1 << 2) ? 0xf << 8 : 0x0;
    output |= a & (1 << 1) ? 0xf << 4 : 0x0;
    output |= a & 1 ? 0xf : 0x0;

    return output;
}

另一种解决方案:

unsigned int eToTW (unsigned char a) {     
    return (a & 1 << 7 ? ((unsigned) 0xf) << 28 : 0x0) | 
           (a & 1 << 6 ? 0xf << 24 : 0x0) | 
           (a & 1 << 5 ? 0xf << 20 : 0x0) | 
           (a & 1 << 4 ? 0xf << 16 : 0x0) | 
           (a & 1 << 3 ? 0xf << 12 : 0x0) |
           (a & 1 << 2 ? 0xf << 8 : 0x0) |
           (a & 1 << 1 ? 0xf << 4 : 0x0) |
           (a & 1 ? 0xf : 0x0);
}

【讨论】:

  • 0xf &lt;&lt; 28 具有未定义的行为:C17 6.5.7 位移位运算符 E1 &lt;&lt; E2 的结果是E1 左移E2 位位置;空出的位用零填充。如果E1 具有无符号类型,则结果的值为E1 × 2E2,比结果类型中可表示的最大值减一模。如果E1 具有带符号类型和非负值,并且E1 × 2E2` 在结果类型中是可表示的,那么这就是结果值;否则,行为未定义。 0xf 的类型为 int0xf &lt;&lt; 28 是 32 位系统上的 UB。使用0xfU 来避免这个问题。
【解决方案4】:

answer by rici 中建议的查找表将在大多数平台上提供最高性能。如果您更喜欢比特旋转的方法,最佳解决方案将取决于您的处理器的硬件功能,例如班次有多快,它是否具有三输入逻辑运算(例如我的 GPU),它可以并行执行多少个整数指令?一种解决方案是将每个位传输到其目标半字节的 lsb,然后在第二步中用其 lsb 值填充每个半字节(向chqrlie 表示建议使用 lsb 而不是 msb):

#include <stdint.h>
uint32_t expand_bits_to_nibbles (uint8_t x)
{
    uint32_t r;
    /* spread bits to lsb in each nibble */
    r = ((((uint32_t)x << (4*0-0)) & (1u << (4*0))) |
         (((uint32_t)x << (4*1-1)) & (1u << (4*1))) |
         (((uint32_t)x << (4*2-2)) & (1u << (4*2))) |
         (((uint32_t)x << (4*3-3)) & (1u << (4*3))) |
         (((uint32_t)x << (4*4-4)) & (1u << (4*4))) |
         (((uint32_t)x << (4*5-5)) & (1u << (4*5))) |
         (((uint32_t)x << (4*6-6)) & (1u << (4*6))) |
         (((uint32_t)x << (4*7-7)) & (1u << (4*7))));
    /* fill in nibbles */
    r = (r << 4) - r;
    return r;
}

使用 Compiler Explorer 进行的一些快速实验表明,这会导致在 PowerPC64 上出现 particularly efficient code

如果处理器有一个快速整数乘法器,我们可以使用它来同时将多个位移动到位。在这里,我们希望使用三个源位组来避免冲突:

#include <stdint.h>
uint32_t expand_bits_to_nibbles_mul (uint8_t x)
{
    const uint32_t spread3 = (1u <<  6) | (1u <<  3) | (1u <<  0);
    const uint8_t bits_lo3 = (1u <<  2) | (1u <<  1) | (1u <<  0);
    const uint8_t bits_md3 = (1u <<  5) | (1u <<  4) | (1u <<  3);
    const uint8_t bits_hi2 = (1u <<  7) | (1u <<  6);
    const uint32_t nib_lsb = (1u << 28) | (1u << 24) | (1u << 20) | (1u << 16) | 
                             (1u << 12) | (1u <<  8) | (1u <<  4) | (1u <<  0);
    uint32_t r;
    /* spread bits to lsb in each nibble */
    r = (((uint32_t)(x & bits_lo3) * (spread3 <<  0)) +
         ((uint32_t)(x & bits_md3) * (spread3 <<  9)) +
         ((uint32_t)(x & bits_hi2) * (spread3 << 18))) & nib_lsb;
    /* fill in nibbles */
    r = (r << 4) - r;
    return r;
}

另一个使用整数乘法的变体,在某些平台上可能更快,它使用了来自this answer 的想法。我们使用乘法一次扩展四个位,使它们落在目标半字节内。但是,我们必须先将半字节内的位移动到半字节的 lsb,然后才能扩展 lsb 以覆盖半字节。我们可能会以额外的内务管理为代价来节省乘法。

#include <stdint.h>
uint32_t expand_bits_to_nibbles_mul2 (uint8_t x)
{
    const uint32_t spread4 = (1u << 12) | (1u <<  8) | (1u <<  4) | (1u <<  0);
    const uint32_t extract = (1u << (3*4+3+16)) | (1u << (2*4+2+16)) | 
                             (1u << (1*4+1+16)) | (1u << (0*4+0+16)) |
                             (1u << (3*4+3+ 0)) | (1u << (2*4+2+ 0)) | 
                             (1u << (1*4+1+ 0)) | (1u << (0*4+0+ 0));
    const uint32_t nib_lsb = (1u << 28) | (1u << 24) | (1u << 20) | (1u << 16) |
                             (1u << 12) | (1u <<  8) | (1u <<  4) | (1u <<  0);
    const uint32_t nib_msb = (nib_lsb << 3);
    const uint8_t bits_lo4 = (1u <<  3) | (1u <<  2) | (1u <<  1) | (1u <<  0);
    const uint8_t bits_hi4 = (1u <<  7) | (1u <<  6) | (1u <<  5) | (1u <<  4);
    uint32_t r;
    /* spread bits to their target nibbles */
    r = (((uint32_t)(x & bits_lo4) * (spread4 <<  0)) +  
         ((uint32_t)(x & bits_hi4) * (spread4 << 12)));
    /* extract appropriate bit in each nibble and move it into nibble's lsb */
    r = (((r & extract) + (nib_msb - extract)) >> 3) & nib_lsb;
    /* fill in each nibble with its lsb */
    r = (r << 4) - r;
    return r;
}

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多