【问题标题】:What is the fastest way in C to extract 8 bits from 8 different bytes?C 中从 8 个不同字节中提取 8 位的最快方法是什么?
【发布时间】:2020-04-07 03:58:14
【问题描述】:

所以我有一个 8 字节的数组,我无法控制,也无法直接更改其格式。此代码是与硬件进行通信的瓶颈,因此优化它很重要。

我的任务是从 8 个源字节中的每个字节中提取 1 个字节的有用数据。我需要从字节中提取的每一位始终处于相同的偏移量。我从最高有效位到最低有效位构建结果字节。

我现在的解决方案如下

const uint8_t MASK = 0x04;

void extract(uint8_t* data, uint8_t* result) {
  // I assume result starts equal to 0

  uint8_t j = 0x80; // Most significant bit first

  for (uint8_t i = 0; i < 8; ++i) {
    // Check if the bit I am interested in is high
    if (data[i] & MASK) {
      // Set the bit in result high
      *result |= j;
    }

    // Move on to the next bit
    j >>= 1;
  }
}

我觉得这已接近最佳状态,但我不擅长使用位魔法,所以我很好奇是否有人知道更快的方法。

代码在 AM335X 上的 TI-PRU 上运行

【问题讨论】:

  • 您的意思是*result |= j;?一种不使用条件的方法是使用 res16 |= (data[i] &amp; mask); res16 &lt;&lt;= 1; 构建一个 16 位值,并在末尾对齐 res16
  • 通过测量进行优化,这意味着确定最快方法的唯一可靠方法是实施您能想到的所有方法并测量哪种方法最快。如果不进行衡量,试图加速优化是徒劳的。
  • 这也取决于微观,有些具有多位移位,例如 x86,有些则没有。
  • 你的代码是not compile,我想这是一个错字。
  • AM335X 是 300MHz ARM Cortex-A8,你认为它“非常有限”?我梦想有这样的马力!更加量化 - 它需要多快(例如每秒比特数)?因为这似乎不太可能成为大多数沟通渠道的瓶颈。

标签: c optimization embedded bit-manipulation bit-shift


【解决方案1】:

假设您的处理器是 32 位处理器。

void extract_shift(uint8_t* data, uint8_t* result) {
    uint32_t x1 = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
    uint32_t x2 = (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7];
    x1 &= (MASK << 24) | (MASK << 16) | (MASK << 8) | (MASK);
    x2 &= (MASK << 24) | (MASK << 16) | (MASK << 8) | (MASK);
    x1 = (x1 >> 19) | (x1 >> 12) | (x1 >>  5) | (x1 <<  2);
    x2 = (x2 >> 23) | (x2 >> 16) | (x2 >>  9) | (x2 >>  2);
    *result = (x1 | x2);
}

这会尝试使用 32 位加载来加载数据(假设您的处理器允许未对齐的加载并且具有正确的字节序,或者编译器能够以更好的方式进行字节交换;x86 上的 gcc does that correctly)。

然后一次使用 32 位字进行屏蔽。

然后通过组合两个半字节来收集不太重要的半字节中的位以完成。这是交错完成的,以尝试限制依赖项的数量。

假设你的机器有一个硬件倍增器,我们可以尝试使用它。如何?乘法是左移的组合。但是这里我们有左移和右移。因此,让我们在最重要的字节中构建结果,然后将其移回原处:

void extract_premul(uint8_t* data, uint8_t* result) {
    uint32_t x1 = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
    uint32_t x2 = (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7];
    x1 &= (MASK << 24) | (MASK << 16) | (MASK << 8) | (MASK);
    x2 &= (MASK << 24) | (MASK << 16) | (MASK << 8) | (MASK);
    x1 = (x1 <<  5) | (x1 << 12) | (x1 << 19) | (x1 << 26);
    x2 = (x2 <<  1) | (x2 <<  8) | (x2 << 15) | (x2 << 22);
    *result = (x1 | x2) >> 24;
}

现在我们可以使用乘法,用二进制表示它们有助于理解与上述版本的关系。

void extract_mul(uint8_t* data, uint8_t* result) {
    uint32_t x1 = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
    uint32_t x2 = (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7];
    x1 &= (MASK << 24) | (MASK << 16) | (MASK << 8) | (MASK);
    x2 &= (MASK << 24) | (MASK << 16) | (MASK << 8) | (MASK);
    //  3         2         1
    // 10987654321098765432109876543210
    x1 *= 0b100000010000001000000100000;
    x2 *=     0b10000001000000100000010;
    *result = (x1 | x2) >> 24;
}

与一组移位相比,两个(可流水线)乘法的相对性能取决于您的硬件。

【讨论】:

  • 这正是我正在寻找的那种解决方案。显然,我将不得不使用不同版本的答案,但我相信某些组合会产生更好的性能。真的很不错的作品!谢谢
  • @MatthewNichols 如果您的处理器是 64 位处理器,您也可以调查直接在 64 位处理器中做同样的事情。
  • PRU 是一个 32 位处理器,它有一个 MAC(Multiply-Accumulate)加速器(一种协处理器)
【解决方案2】:

提供的代码足够高效,但如果您对替代方案感兴趣,首先您可以通过手动展开来摆脱循环。其次,您可以用一些位旋转替换if 逻辑:

j = (!!(data[0] & MASK)) << 7;
j |= (!!(data[1] & MASK)) << 6;
...
j |= (!!(data[6] & MASK)) << 1;
j |= (!!(data[7] & MASK));

同样,我认为生成的代码不会比启用优化的原始代码更好。

【讨论】:

  • 我不确定这段代码是否比原始代码更好。看看here 真的很有趣。但毕竟也许它更快,因为没有条件跳跃
  • @Jabberwocky 甚至没有一个! 做一个条件? ! 不是 ~
  • @Jabberwocky 是的,我同意并在答案中指出了这一点。对,看起来我的代码在这种特定情况下是无分支的,但对于其他架构可能会有所不同
  • @WeatherVane 您可以将!! 替换为bool,效果相同。
  • @WeatherVane !! 确保结果为 0 或 1。
【解决方案3】:

假设 MASK 是一个常数 0x04:

然后读入两个掩码的 32 位值,例如 @AProgrammer:

uint32_t* dwptr = (uint32_t*)data;
uint32_t x1 = dwptr[0] & 0x04040404;
uint32_t x2 = dwptr[1] & 0x04040404;

相关位设置在 2 个变量的每个字节的第 2 位上。

将位移动到方便的位置(对于前 4 个字节,我们需要结果的高 4 位作为标志,因此对于 x1 将其从位 2 移动到位 4 - x2 已经在低 4 位,我们将补偿 x2 在下一个块之后的块中的第 2 位而不是第 0 位):

x1 <<= 2;

通过向左移动和 ORing 将这些位四倍:

x1 |= x1 << 1;
x1 |= x1 << 2;
x2 |= x2 << 1;
x2 |= x2 >> 2; // we started on bit 2 and not bit 0 for x2 - saved us the shift of x2 in the block above

现在删除不需要的:

x1 &= 0x10204080;
x2 &= 0x01020408;

构建结果(组合所有 8 个字节):

x1_8* = (uint8_t*)x1;
x1_16* = (uint16_t*)x1;

x1 |= x2;
x1_16[0] |= x1_16[1];
result = x1_8[0] | x1_8[1];

我写了很多行代码以使其易于理解,但它应该运行得相当快 - 对于所有 8 位,我们总共只有 5 个移位和 9 个逻辑操作。


你也可以试试汇编程序,比如

  • 结果是例如R0.b0(寄存器 0,字节 0)
  • MASKBIT 为 2,MASK 为常数 0x04
  • byte0/byte1/byte2 被加载到寄存器中,例如R1.b0、R1.b1、R1.b2、R1.b3、R2.b0、R2.b1、R2.b2、R2.b3,如果加载到寄存器 R1 和 R2
        ldi result, 0
        qbbc flag0zero, byte0, MASKBIT
        set result, result, 7
    flag0zero:
        qbbc flag1zero, byte1, MASKBIT
        set result, result, 6
    flag1zero:
        qbbc flag2zero, byte2, MASKBIT
        set result, result, 5
    flag2zero:

等等。

PRU 可以在 1 个周期内完成所有这些内部操作,甚至是组合的 bittests/jumps。我们有 8 个位测试和 8 个位集。在汇编程序中执行之前的算法可能会更快一些,因为分配在 RISC 架构上是“隐藏的”,因为您可以将目标寄存器与源寄存器分开指定。


可能访问内存或提到的硬件外围设备比计算标志慢。我们谈论的是在 200 MHz 下进行约 20 次操作,即 100ns 用于完整的标志计算。

您可以启用循环计数器 (https://nerdhut.de/2016/06/18/beaglebone-clock-cycle-counter/) 来测量什么需要多长时间以及什么解决方案最快。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多