【问题标题】:How to combine two 16bit-words into one 32bit-word bit by bit efficiently?如何高效地将两个 16 位字逐位组合成一个 32 位字?
【发布时间】:2014-02-28 16:58:24
【问题描述】:

我必须将两个 16 位字组合成一个 32 位字数百次,这需要大量的计算能力。我想找到一种更有效的方法来做到这一点。

我有 2 个名为 A 和 B 的 16 位字。我想要一个名为 C 的 32 位字。A 中的位应复制到 C 中的偶数位。B 中的位应复制到奇数位C中的位数。例如:A:0b0000000000000000 B:0b1111111111111111处理后的C应该是0b10101010101010101010101010101010。

我目前的解决方案是这样的:

for (i = 0; i < 32; i+=2)
{
    C |=  (A & (1 << (i/2))) << (i/2);
    C |=  (B & (1 << (i/2))) << (i/2 + 1);
}

当我要处理数百个 C 时,此解决方案需要太多时间。我正在寻找更好的!

添加:此程序在 TriCore 上运行。我别无选择,只能以这种方式处理数据,因为 AB 和 C 之间的这种关系是由协议定义的。

谢谢!

【问题讨论】:

  • 你在用什么处理器?
  • 我们真的必须知道我们在谈论什么嵌入式MCU。它有桶形移位器吗?它是否具有必须手动使用的能够进行桶形移位的外围设备?
  • 从长远来看,也许你应该回顾一下为什么你有这些数据输入,看看你能做些什么来改变事情,这样你就不必做如此扭曲的操作序列。粗略地说:为什么数据会以如此粗鲁的方式呈现,因为它必然会使处理速度变慢。
  • 我不会点击 TriCore 许可协议来阅读他们的文档,但它是 32 位的,并且 RISC 架构具有桶形移位器并不少见,所以你的 C 可能是好的。你看过它的拆解吗?

标签: c performance embedded bitwise-operators


【解决方案1】:

原来 Tricore 有一个 BMERGE 指令,它可以精确地执行您想要的操作 - 它需要两个 16 位值并交错位。如果您使用基于 gcc 的工具链,您应该能够使用单个内联 asm 语句——类似于:

asm("bmerge %0,%1,%2" : "=r"(C) : "r"(A), "r"(B))

还有一个 BSPLIT 指令可以做相反的事情。

【讨论】:

    【解决方案2】:

    不是循环,而是分组转移。

    可能还有一些进一步的简化,但以下是它的要点。平均速度更快(或最坏情况)?配置文件找出来。

    #include <inttypes.h>
    #include <stdint.h>
    
    uint64_t Merge(uint32_t a, uint32_t b) {
      uint64_t A,B;
      A = ((a & 0x00000000FFFF0000ull) << 16) | (a & 0x000000000000FFFFull);
      A = ((A & 0x0000FF000000FF00ull) <<  8) | (A & 0x000000FF000000FFull);
      A = ((A & 0xF0F0F0F0F0F0F0F0ull) <<  4) | (A & 0x0F0F0F0F0F0F0F0Full);
      A = ((A & 0xCCCCCCCCCCCCCCCCull) <<  2) | (A & 0x0333333333333333ull);
      A = ((A & 0xAAAAAAAAAAAAAAAAull) <<  1) | (A & 0x5555555555555555ull);
    
      B = ((b & 0x00000000FFFF0000ull) << 16) | (b & 0x000000000000FFFFull);
      B = ((B & 0x0000FF000000FF00ull) <<  8) | (B & 0x000000FF000000FFull);
      B = ((B & 0xF0F0F0F0F0F0F0F0ull) <<  4) | (B & 0x0F0F0F0F0F0F0F0Full);
      B = ((B & 0xCCCCCCCCCCCCCCCCull) <<  2) | (B & 0x0333333333333333ull);
      B = ((B & 0xAAAAAAAAAAAAAAAAull) <<  1) | (B & 0x5555555555555555ull);
    
      return A | (B << 1);
    }
    
    void MergeTest(uint32_t a, uint32_t b) {
      uint64_t C = Merge(a,b);
      printf("a:%08" PRIX32 " b:%08" PRIX32 " c:%016" PRIX64 "\n", a,b,C);
    }
    
    void MergeTests(void) {
      MergeTest(0x00000000L, 0xFFFFFFFFL);
      MergeTest(0xFFFFFFFFL, 0x00000000L);
      MergeTest(0x00000000L, 0x00000001L);;
      MergeTest(0x00000000L, 0x00000010L);;
    }
    
    a:00000000 b:FFFFFFFF c:AAAAAAAAAAAAAAAA  
    a:FFFFFFFF b:00000000 c:5555555555555555  
    a:00000000 b:00000001 c:0000000000000002  
    a:00000000 b:00000010 c:0000000000000200  
    

    【讨论】:

      【解决方案3】:

      试试这个:

      for (i = 0; i < 32; i+=2)
      {
          int i2 = i >> 1 ;
          int andval = 1 << i2 ;
          C |=  (A & andval) << i2;
          C |=  (B & andval) << (i2 + 1);
      }
      

      但是你的编译器可能已经完成了这个优化。

      【讨论】:

        【解决方案4】:

        在 MCU(可能是 8 位并且可能没有桶形移位器)上工作的最有可能的解决方案类型是按照这些线手工编码组装(采用 ABCL/CH 作为 16 位寄存器):

        LOOP:
          MOV CNT, 16
          RRC A     ; rotate A right through the carry
          RRC CH    ; carry enters C at the top
          RRC CL    ; continue roll through CL
          RRC B
          RRC CH
          RRC CL
          DJNZ CNT,LOOP
        

        (如果 MCU 是 8 位,显然每个 RRC 变成两个)。

        此解决方案将位“混洗”在一起,同时每个周期仅旋转一位,这是任何 MCU 都可以做到的。你可以尝试用 C 来编写它,但你需要一个非常好的优化器来从 lsb = A &amp; 1; A &gt;&gt;= 1; C &gt;&gt;=1; C |= lsb &lt;&lt; 31; 之类的东西生成这个指令序列

        编辑:使用 32 位 CPU,您可以考虑Bit Twiddling Hacks 中列出的所有选项。

        【讨论】:

          【解决方案5】:

          似乎快了 40%,但实际上取决于编译器优化 ;-)

          for (i=1, j=2, msk=1; i<0x100000000; i<<=2, j<<=2, msk<<=1) {
              if (A & msk) C |= i;
              if (B & msk) C |= j;
          }
          

          【讨论】:

            【解决方案6】:

            这个问题也称为“莫顿数编码”;即将 2-D 或 3-D 坐标展平为单个数字。

            blog entry 总结了三种典型方法:naïve for 循环、魔术位(如 chux 的答案)和查找表。基于 LUT 的方法显然是赢家。

            基本上必须选择一次处理多少位。通常,最佳位置在 8->16 位或 4->8 位 LUT 中,例如这里。

            0001 --> 0 0 0 0 0 0 0 1
            0010 --> 0 0 0 0 0 1 0 0
            0011 --> 0 0 0 0 0 1 0 1  etc.
            

            用这个表扩展两个uint8_t变量是用公式实现的:

            uint16_t ans =  LUT[a & 15]       + (LUT[b & 15] << 1) +
                           (LUT[a >> 4] << 8) + (LUT[b << 4] << 9);
            

            同样,我们必须分析在给定位数的情况下,拥有 4 个不同的表是否更有效,每个表左移一个常数,还是手动执行移位。

            【讨论】:

              【解决方案7】:

              下面使用两个walk-one mask,一个用于测试源数据位,一个用于掩蔽到目标。在 compileonline.com 进行 1000 万次迭代的测试得出以下结果:

              • 原始算法:1.14 秒
              • 此算法:0.81 秒

              尽管不要停止阅读 - 接下来会有显着的改进。

                  uint32_t C ;
                  uint16_t srcmask ;
                  uint32_t dstmask ;
              
                  for( C = 0, srcmask = 1u, dstmask = 1u; 
                       srcmask != 0; 
                       srcmask <<= 1 )
                  {
                      if( (A & srcmask) != 0 )
                      {
                          C |= dstmask ;
                      }
                      dstmask <<= 1 ;
              
                      if( (B & srcmask) != 0 )
                      {
                          C |= dstmask ;
                      }
                      dstmask <<= 1 ;
                  }
              

              然而,理论上,性能可能会因 1 位的数量而异,但在我的测试中,这种差异无法测量,但不同的目标和编译器可能会产生不同的结果。

              每次迭代将循环展开到 4 个源位具有边际优势(0.77 秒):

                  for( C = 0, srcmask = 1u, dstmask = 1u; 
                       srcmask != 0; 
                       srcmask <<= 1 )
                  {
                      // Unroll 1
                      if( (A & srcmask) )
                      {
                          C |= dstmask ;
                      }
                      dstmask <<= 1 ;
              
                      if( (B & srcmask) )
                      {
                          C |= dstmask ;
                      }
                      dstmask <<= 1 ;
              
                      // Unroll 2
                      srcmask <<= 1 ;
                      if( (A & srcmask) )
                      {
                          C |= dstmask ;
                      }
                      dstmask <<= 1 ;
              
                      if( (B & srcmask) )
                      {
                          C |= dstmask ;
                      }
                      dstmask <<= 1 ;
              
                      // Unroll 3
                      srcmask <<= 1 ;
                      if( (A & srcmask) )
                      {
                          C |= dstmask ;
                      }
                      dstmask <<= 1 ;
              
                      if( (B & srcmask) )
                      {
                          C |= dstmask ;
                      }
                      dstmask <<= 1 ;
              
                      // Unroll 4
                      srcmask <<= 1 ;
                      if( (A & srcmask) )
                      {
                          C |= dstmask ;
                      }
                      dstmask <<= 1 ;
              
                      if( (B & srcmask) )
                      {
                          C |= dstmask ;
                      }
                      dstmask <<= 1 ;
                  }
              

              进一步展开会产生不利影响,但目标和编译器的结果可能会有所不同。

              然后我将Csrcmaskdstmask 声明为register,没想到会有任何区别:

              register uint32_t C ;
              register uint16_t srcmask ;
              register uint32_t dstmask ;
              

              我对结果感到震惊:

              • 原始算法:1.19 秒
              • 此算法:0.29

              此处展开的效果非常显着 - 没有它,时间变为 0.45 秒,而 2x 展开 = 0.33 秒。进一步展开的影响很小。将 A 和 B 声明为寄存器会稍微降低性能 - 只有这么多寄存器可以使用!又是 YMMV。

              因此,结论一定是您需要尝试多种技术来确定最适合您的目标的方法。在这里,更好的算法、循环展开和寄存器变量的组合产生了巨大的影响。尝试不同的编译器优化设置也可能会产生影响,尽管改进某个代码区域可能会损害其他代码,因此您可能不想对所有代码应用相同的优化。

              【讨论】:

                猜你喜欢
                • 2017-04-19
                • 1970-01-01
                • 1970-01-01
                • 2019-08-26
                • 2016-06-01
                • 1970-01-01
                • 1970-01-01
                • 2022-01-17
                • 1970-01-01
                相关资源
                最近更新 更多