2

我有一个类型unsigned char(即)的大型数组(大约 1 MB uint8_t)。我知道其中的字节只能有 5 个值之一(即 0、1、2、3、4)。此外,我们不需要从输入中保留“3”,当我们编码/解码时,它们可以安全地丢失。

所以我猜测位打包将是最简单的压缩方法,因此每个字节都可以转换为 2 位(00, 01..., 11)。

如前所述,可以删除所有值为 3 的元素(即保存为 0)。这让我可以选择将“4”保存为“3”。在重建(解压缩)时,我将 3 恢复为 4。

我为压缩编写了一个小函数,但我觉得它的操作太多,效率不够。任何关于如何使其更高效或更快(希望保持可读性)的代码片段或建议都会非常有帮助。

/// Compress by packing ...
void compressByPacking (uint8_t* out, uint8_t* in, uint32_t length)
{
    for (int loop = 0; loop < length/4; loop ++, in += 4, out++)
    {
      uint8_t temp[4];

      for (int small_loop = 0; small_loop < 4; small_loop++)
      {
        temp[small_loop] = *in;           // Load into local variable

        if (temp[small_loop] == 3)        // 3's are discarded
          temp[small_loop] = 0;
        else if (temp[small_loop] == 4)   // and 4's are converted to 3
          temp[small_loop] = 3;

      } // end small loop

      // Pack the bits into write pointer
      *out = (uint8_t)((temp[0] & 0x03) << 6) |
                      ((temp[1] & 0x03) << 4) |
                      ((temp[2] & 0x03) << 2) |
                      ((temp[3] & 0x03));

    } // end loop
 }
  • 编辑以使问题更清楚,因为看起来我正在尝试将 5 个值保存到 2 位中。感谢@Brian Cain 的建议措辞。
  • 交叉发布在代码审查上。
4

5 回答 5

3

您的函数有一个错误:加载小数组时,您应该编写:

    temp[small_loop] = in[small_loop];

您可以使用查找表摆脱对源数据的测试,或者更有效地对某些中间结果进行测试:

在下面的代码中,我使用一个小表lookup5将值转换0,1,2,3,40,1,2,0,3,并使用一个较大的表将源数组中的 4 个 3 位值组映射到打包格式的相应字节值:

#include <stdint.h>

/// Compress by packing ...
void compressByPacking0(uint8_t *out, uint8_t *in, uint32_t length) {
    static uint8_t lookup[4096];
    static const uint8_t lookup5[8] = { 0, 1, 2, 0, 3, 0, 0, 0 };

    if (lookup[0] == 0) {    
        /* initialize lookup table */
        for (int i = 0; i < 4096; i++) {
            lookup[i] = (lookup5[(i >> 0) & 7] << 0) +
                        (lookup5[(i >> 3) & 7] << 2) +
                        (lookup5[(i >> 6) & 7] << 4) +
                        (lookup5[(i >> 9) & 7] << 6);
        }
    }
    for (; length >= 4; length -= 4, in += 4, out++) {
         *out = lookup[(in[0] << 9) + (in[1] << 6) + (in[2] << 3) + (in[3] << 0)];
    }
    uint8_t last = 0;
    switch (length) {
      case 3:
        last |= lookup5[in[2]] << 4;
        /* fall through */
      case 2:
        last |= lookup5[in[1]] << 2;
        /* fall through */
      case 1:
        last |= lookup5[in[0]] << 0;
        *out = last;
        break;
    }
}

笔记:

  • 该代码假定数组不包含指定范围之外的值。可以以最低的成本实现对虚假输入的额外保护。

  • 假人<< 0在这里只是为了对称并且编译为没有额外的代码。

  • 查找表可以通过构建时脚本或一组宏静态初始化。

  • 您可能希望将此循环展开 4 次或更多次,或者让编译器决定。

您还可以使用这个更简单的解决方案和更频繁访问的较小查找表。仔细的基准测试会告诉您哪个对您的目标系统更有效:

/// Compress by packing ...
void compressByPacking1(uint8_t *out, uint8_t *in, uint32_t length) {
    static const uint8_t lookup[4][5] = {
        { 0 << 6, 1 << 6, 2 << 6, 0 << 6, 3 << 6 },
        { 0 << 4, 1 << 4, 2 << 4, 0 << 4, 3 << 4 },
        { 0 << 2, 1 << 2, 2 << 2, 0 << 2, 3 << 2 },
        { 0 << 0, 1 << 0, 2 << 0, 0 << 0, 3 << 0 },
    };

    for (; length >= 4; length -= 4, in += 4, out++) {
         *out = lookup[0][in[0]] + lookup[1][in[1]] +
                lookup[2][in[2]] + lookup[3][in[3]];
    }
    uint8_t last = 0;
    switch (length) {
      case 3:
        last |= lookup[2][in[2]];
        /* fall through */
      case 2:
        last |= lookup[1][in[1]];
        /* fall through */
      case 1:
        last |= lookup[0][in[0]];
        *out = last;
        break;
    }
}

这是另一种方法,没有任何表格:

/// Compress by packing ...
void compressByPacking2(uint8_t *out, uint8_t *in, uint32_t length) {
#define BITS ((1 << 2) + (2 << 4) + (3 << 8))
    for (; length >= 4; length -= 4, in += 4, out++) {
         *out = ((BITS << 6 >> (in[0] + in[0])) & 0xC0) +
                ((BITS << 4 >> (in[1] + in[1])) & 0x30) +
                ((BITS << 2 >> (in[2] + in[2])) & 0x0C) +
                ((BITS << 0 >> (in[3] + in[3])) & 0x03);
    }
    uint8_t last = 0;
    switch (length) {
      case 3:
        last |= (BITS << 2 >> (in[2] + in[2])) & 0x0C;
        /* fall through */
      case 2:
        last |= (BITS << 4 >> (in[1] + in[1])) & 0x30;
        /* fall through */
      case 1:
        last |= (BITS << 6 >> (in[0] + in[0])) & 0xC0;
        *out = last;
        break;
    }
}

这是我的系统上的比较基准,Macbook pro 运行 OS/X,具有clang -O2

compressByPacking(1MB) -> 0.867ms
compressByPacking0(1MB) -> 0.445ms
compressByPacking1(1MB) -> 0.538ms
compressByPacking2(1MB) -> 0.824ms

compressByPacking0变体最快,几乎是您的代码的两倍。这有点令人失望,但代码是可移植的。您可能会使用手动编码的 SSE 优化来提高性能。

于 2017-07-18T20:59:11.393 回答
1

在所有关于性能的兴奋中,功能被忽视了。代码坏了。

    // temp[small_loop] = *in;           // Load into local variable
    temp[small_loop] = in[small_loop]; 

选择:

一个简单的紧循环怎么样?

使用constrestrict允许各种优化。

void compressByPacking1(uint8_t* restrict out, const uint8_t* restrict in,
    uint32_t length) {
  static const uint8_t t[5] = { 0, 1, 2, 0, 3 };
  uint32_t length4 = length / 4;
  unsigned v = 0;
  uint32_t i;
  for (i = 0; i < length4; i++) {
    for (unsigned j=0; j < 4; j++) {
      v <<= 2;
      v |= t[*in++];
    }
    out[i] = (uint8_t) v;
  }
  if (length & 3) {
    v = 0;
    for (unsigned j; j < 4; j++) {
      v <<= 2;
      if (j < (length & 3)) {
        v |= t[*in++];
      }
    }
    out[i] = (uint8_t) v;
  }
}

测试并发现此代码的速度大约是 270%(41 对 15)(YMMV)。
测试并发现形成与 OP(更正)代码相同的输出

于 2017-07-18T23:07:24.290 回答
1

我有一个大数组(大约 1 MB)

要么这是一个错字,要么你的目标严重老化,要么这个压缩操作在你的应用程序的关键路径中被重复调用。

任何关于如何使其更高效或更快(希望保持可读性)的代码片段或建议都会非常有帮助。

通常,您会通过经验性地测量性能和检查生成的代码来找到最佳信息。使用分析器来确定正在执行的代码、缓存未命中和管道停顿的位置——这些可以帮助您调整算法。

例如,您选择了 4 个元素的步幅。这仅仅是因为您将四个输入元素映射到一个字节吗?您可以使用本机 SIMD 指令/内在函数一次对更多元素进行操作吗?

另外,你是如何为你的目标编译的,你的编译器对你的代码的优化能力如何?

让我们问clang一下它是否在尝试优化您的代码时发现了任何问题:

$ clang -fvectorize  -O3  -Rpass-missed=licm -c tryme.c 
tryme.c:11:28: remark: failed to move load with loop-invariant address because the loop may invalidate its value [-Rpass-missed=licm]
        temp[small_loop] = *in;           // Load into local variable
                           ^
tryme.c:21:25: remark: failed to move load with loop-invariant address because the loop may invalidate its value [-Rpass-missed=licm]
      *out = (uint8_t)((temp[0] & 0x03) << 6) |
                        ^
tryme.c:22:25: remark: failed to move load with loop-invariant address because the loop may invalidate its value [-Rpass-missed=licm]
                      ((temp[1] & 0x03) << 4) |
                        ^
tryme.c:23:25: remark: failed to move load with loop-invariant address because the loop may invalidate its value [-Rpass-missed=licm]
                      ((temp[2] & 0x03) << 2) |
                        ^
tryme.c:24:25: remark: failed to move load with loop-invariant address because the loop may invalidate its value [-Rpass-missed=licm]
                      ((temp[3] & 0x03));
                        ^

我不确定,但也许别名分析是让它认为它无法移动这个负载的原因。试试看__restrict__有没有效果。

$ clang -fvectorize  -O3  -Rpass-analysis=loop-vectorize  -c tryme.c 
tryme.c:13:13: remark: loop not vectorized: loop contains a switch statement [-Rpass-analysis=loop-vectorize]
        if (temp[small_loop] == 3)        // 3's are discarded

除非你改变你的算法,否则我想不出任何明显的事情可以解决这个问题。如果在不删除 s 的情况下压缩比令人满意3,您也许可以消除它。

那么生成的代码是什么样的呢?看看下面。你怎么能更好地手写呢?如果您自己可以更好地编写它,要么这样做,要么将其反馈到您的算法中以帮助指导编译器。

编译后的代码是否利用了目标的指令集和寄存器?

最重要的是——尝试执行它,看看你在哪里花费了最多的周期。因分支错误预测、未对齐负载而导致的停顿?也许你可以对这些做点什么。使用您对输入数据频率的了解为编译器提供有关编码器分支的提示。

$ objdump -d --source tryme.o
...
0000000000000000 <compressByPacking>:
#include <stdint.h>

void compressByPacking (uint8_t* out, uint8_t* in, uint32_t length)
{
    for (int loop = 0; loop < length/4; loop ++, in += 4, out++)
   0:   c1 ea 02                shr    $0x2,%edx
   3:   0f 84 86 00 00 00       je     8f <compressByPacking+0x8f>
   9:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
    {
      uint8_t temp[4];

      for (int small_loop = 0; small_loop < 4; small_loop++)
      {
        temp[small_loop] = *in;           // Load into local variable
  10:   8a 06                   mov    (%rsi),%al

        if (temp[small_loop] == 3)        // 3's are discarded
  12:   3c 04                   cmp    $0x4,%al
  14:   74 3a                   je     50 <compressByPacking+0x50>
  16:   3c 03                   cmp    $0x3,%al
  18:   41 88 c0                mov    %al,%r8b
  1b:   75 03                   jne    20 <compressByPacking+0x20>
  1d:   45 31 c0                xor    %r8d,%r8d
  20:   3c 04                   cmp    $0x4,%al
  22:   74 33                   je     57 <compressByPacking+0x57>
  24:   3c 03                   cmp    $0x3,%al
  26:   88 c1                   mov    %al,%cl
  28:   75 02                   jne    2c <compressByPacking+0x2c>
  2a:   31 c9                   xor    %ecx,%ecx
  2c:   3c 04                   cmp    $0x4,%al
  2e:   74 2d                   je     5d <compressByPacking+0x5d>
  30:   3c 03                   cmp    $0x3,%al
  32:   41 88 c1                mov    %al,%r9b
  35:   75 03                   jne    3a <compressByPacking+0x3a>
  37:   45 31 c9                xor    %r9d,%r9d
  3a:   3c 04                   cmp    $0x4,%al
  3c:   74 26                   je     64 <compressByPacking+0x64>
  3e:   3c 03                   cmp    $0x3,%al
  40:   75 24                   jne    66 <compressByPacking+0x66>
  42:   31 c0                   xor    %eax,%eax
  44:   eb 20                   jmp    66 <compressByPacking+0x66>
  46:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  4d:   00 00 00 
  50:   41 b0 03                mov    $0x3,%r8b
  53:   3c 04                   cmp    $0x4,%al
  55:   75 cd                   jne    24 <compressByPacking+0x24>
  57:   b1 03                   mov    $0x3,%cl
  59:   3c 04                   cmp    $0x4,%al
  5b:   75 d3                   jne    30 <compressByPacking+0x30>
  5d:   41 b1 03                mov    $0x3,%r9b
  60:   3c 04                   cmp    $0x4,%al
  62:   75 da                   jne    3e <compressByPacking+0x3e>
  64:   b0 03                   mov    $0x3,%al
          temp[small_loop] = 3;

      } // end small loop

      // Pack the bits into write pointer
      *out = (uint8_t)((temp[0] & 0x03) << 6) |
  66:   41 c0 e0 06             shl    $0x6,%r8b
                      ((temp[1] & 0x03) << 4) |
  6a:   c0 e1 04                shl    $0x4,%cl
  6d:   80 e1 30                and    $0x30,%cl
          temp[small_loop] = 3;

      } // end small loop

      // Pack the bits into write pointer
      *out = (uint8_t)((temp[0] & 0x03) << 6) |
  70:   44 08 c1                or     %r8b,%cl
                      ((temp[1] & 0x03) << 4) |
                      ((temp[2] & 0x03) << 2) |
  73:   41 c0 e1 02             shl    $0x2,%r9b
  77:   41 80 e1 0c             and    $0xc,%r9b
                      ((temp[3] & 0x03));
  7b:   24 03                   and    $0x3,%al

      } // end small loop

      // Pack the bits into write pointer
      *out = (uint8_t)((temp[0] & 0x03) << 6) |
                      ((temp[1] & 0x03) << 4) |
  7d:   44 08 c8                or     %r9b,%al
                      ((temp[2] & 0x03) << 2) |
  80:   08 c8                   or     %cl,%al
          temp[small_loop] = 3;

      } // end small loop

      // Pack the bits into write pointer
      *out = (uint8_t)((temp[0] & 0x03) << 6) |
  82:   88 07                   mov    %al,(%rdi)
#include <stdint.h>

void compressByPacking (uint8_t* out, uint8_t* in, uint32_t length)
{
    for (int loop = 0; loop < length/4; loop ++, in += 4, out++)
  84:   48 83 c6 04             add    $0x4,%rsi
  88:   48 ff c7                inc    %rdi
  8b:   ff ca                   dec    %edx
  8d:   75 81                   jne    10 <compressByPacking+0x10>
                      ((temp[1] & 0x03) << 4) |
                      ((temp[2] & 0x03) << 2) |
                      ((temp[3] & 0x03));

    } // end loop
 }
  8f:   c3                      retq   
于 2017-07-18T21:10:56.660 回答
-1

更新:经过测试

不安全版本是最快的 - 比其他答案中的其他版本最快。用VS2017测试

const uint8_t table[4][5] = 
{ { 0 << 0,1 << 0,2 << 0,0 << 0,3 << 0 },
  { 0 << 2,1 << 2,2 << 2,0 << 2,3 << 2 },
  { 0 << 4,1 << 4,2 << 4,0 << 4,3 << 4 },
  { 0 << 6,1 << 6,2 << 6,0 << 6,3 << 6 },
};



void code(uint8_t *in, uint8_t *out, uint32_t len)
{
    memset(out, 0, len / 4 + 1);
    for (uint32_t i = 0; i < len; i++)
        out[i / 4] |= table[i & 3][in[i] % 5];
}

void code_unsafe(uint8_t *in, uint8_t *out, uint32_t len)
{
    for (uint32_t i = 0; i < len; i += 4, in += 4, out++)
    {
        *out = table[0][in[0]] | table[1][in[1]] | table[2][in[2]] | table[3][in[3]];
    }
}

要检查它是如何编写的,编译它就足够了——即使是在线的

https://godbolt.org/g/Z75NQV

我的编码有很小很简单的功能——只是为了比较编译器生成的代码,没有经过测试。

于 2017-07-18T21:15:59.113 回答
-2

这看起来更清楚吗?

void compressByPacking (uint8_t* out, uint8_t* in, uint32_t length)
{
    assert( 0 == length % 4 );
    for (int loop = 0; loop < length; loop += 4)
    {
      uint8_t temp = 0;
      for (int small_loop = 0; small_loop < 4; small_loop++)
      {
        uint8_t inv = *in;  // get next input value
        switch(inv)
        {
          case 0:  // encode as 00
          case 3:  // change to 0
             break;
          case 1:
              temp |= (1 << smal_loop*2); // 1 encode as '01'
              break;
          case 2:
              temp |= (2 << smal_loop*2);  // 2 encode as '10'
              break;
          case 4:
              temp |= (3 << smal_loop*2);  // 4 encode as '11'
              break;
          default:
              assert(0);
        }
      } // end inner loop

      *out = temp;

    } // end outer loop
 }
于 2017-07-18T20:49:01.863 回答