1

我目前正在研究在纯 C 中实现高效 BitStream 的各种可能策略。我需要它来实现各种基于位的压缩算法。但是,我找不到太多关于该主题的文献,而且似乎也找不到很多好的例子。

这是我正在寻找的:

  • 主要是基于宏的实现以避免函数调用
  • 向/从 BitStream 读取/写入“n”个比特的函数。
  • 读取/写入特定位数(如 5 位)的功能,在通用位上进行了优化。

我想知道以下几点:

  • 应在 BitStream 中维护的变量。可以有一个 BYTE 指针、一个字节位置、当前字节中的位索引、当前字节中剩余的位数等。
  • 如何减少要维护的变量数量。我们拥有的变量越多,我们需要更新的变量就越多。
  • 如何在单个读/写操作的上下文中使用少量中间/临时变量。
  • 是否应该在 BYTE 级别或 UINT16 级别或 UINT32 级别进行操作。也许将位累积到 UINT32 并在它已满时写入字节(或在写入完成时,使用刷新操作)会比按字节执行所有操作要快得多。
  • 我们怎样才能尽可能地避免循环。理想情况下,我们应该不惜一切代价避免循环写入比特流的位数。

这可能看起来有点矫枉过正,但是当涉及到压缩的其余代码已经被极度优化时,看起来 BitStream 部分只是破坏了整个事情。例如,在图像压缩代码中使用 SIMD CPU 指令来优化部分编码过程的汇编例程并不罕见,但最后一步是写入 BitStream。

想法,参考,任何人?谢谢!

4

2 回答 2

3

作为记录,这是我最终得到的 BitStream 实现。它主要基于宏,并使用 32 位累加器。位从最高有效位到最低有效位存储在累加器中。例如,要测试是否设置了下一位,可以执行 (accumulator & 0x80000000)。这使得无需过多操作 BitStream 即可进行多个测试非常实用。对于写入,我在累加器中累积位,并在输出缓冲区已满时自动将它们刷新到输出缓冲区。当所有位都已写入时,也可以手动触发刷新。您的里程可能会有所不同,但我对此非常满意。我用它实现了微软点对点压缩(MPPC),它使用霍夫曼编码,性能很好。

struct _wBitStream
{
    BYTE* buffer;
    BYTE* pointer;
    DWORD position;
    DWORD length;
    DWORD capacity;
    UINT32 mask;
    UINT32 offset;
    UINT32 prefetch;
    UINT32 accumulator;
};
typedef struct _wBitStream wBitStream;

#define BitStream_Prefetch(_bs) do { \
    (_bs->prefetch) = 0; \
    if (((UINT32) (_bs->pointer - _bs->buffer)) < (_bs->capacity + 4)) \
        (_bs->prefetch) |= (*(_bs->pointer + 4) << 24); \
    if (((UINT32) (_bs->pointer - _bs->buffer)) < (_bs->capacity + 5)) \
        (_bs->prefetch) |= (*(_bs->pointer + 5) << 16); \
    if (((UINT32) (_bs->pointer - _bs->buffer)) < (_bs->capacity + 6)) \
        (_bs->prefetch) |= (*(_bs->pointer + 6) << 8); \
    if (((UINT32) (_bs->pointer - _bs->buffer)) < (_bs->capacity + 7)) \
        (_bs->prefetch) |= (*(_bs->pointer + 7) << 0); \
} while(0)

#define BitStream_Fetch(_bs) do { \
    (_bs->accumulator) = 0; \
    if (((UINT32) (_bs->pointer - _bs->buffer)) < (_bs->capacity + 0)) \
        (_bs->accumulator) |= (*(_bs->pointer + 0) << 24); \
    if (((UINT32) (_bs->pointer - _bs->buffer)) < (_bs->capacity + 1)) \
        (_bs->accumulator) |= (*(_bs->pointer + 1) << 16); \
    if (((UINT32) (_bs->pointer - _bs->buffer)) < (_bs->capacity + 2)) \
        (_bs->accumulator) |= (*(_bs->pointer + 2) << 8); \
    if (((UINT32) (_bs->pointer - _bs->buffer)) <(_bs->capacity + 3)) \
        (_bs->accumulator) |= (*(_bs->pointer + 3) << 0); \
    BitStream_Prefetch(_bs); \
} while(0)

#define BitStream_Flush(_bs) do { \
    if (((UINT32) (_bs->pointer - _bs->buffer)) < (_bs->capacity + 0)) \
        *(_bs->pointer + 0) = (_bs->accumulator >> 24); \
    if (((UINT32) (_bs->pointer - _bs->buffer)) < (_bs->capacity + 1)) \
        *(_bs->pointer + 1) = (_bs->accumulator >> 16); \
    if (((UINT32) (_bs->pointer - _bs->buffer)) < (_bs->capacity + 2)) \
        *(_bs->pointer + 2) = (_bs->accumulator >> 8); \
    if (((UINT32) (_bs->pointer - _bs->buffer)) < (_bs->capacity + 3)) \
        *(_bs->pointer + 3) = (_bs->accumulator >> 0); \
} while(0)

#define BitStream_Shift(_bs, _nbits) do { \
    _bs->accumulator <<= _nbits; \
    _bs->position += _nbits; \
    _bs->offset += _nbits; \
    if (_bs->offset < 32) { \
        _bs->mask = ((1 << _nbits) - 1); \
        _bs->accumulator |= ((_bs->prefetch >> (32 - _nbits)) & _bs->mask); \
        _bs->prefetch <<= _nbits; \
    } else { \
        _bs->mask = ((1 << _nbits) - 1); \
        _bs->accumulator |= ((_bs->prefetch >> (32 - _nbits)) & _bs->mask); \
        _bs->prefetch <<= _nbits; \
        _bs->offset -= 32; \
        _bs->pointer += 4; \
        BitStream_Prefetch(_bs); \
        if (_bs->offset) { \
            _bs->mask = ((1 << _bs->offset) - 1); \
            _bs->accumulator |= ((_bs->prefetch >> (32 - _bs->offset)) & _bs->mask); \
            _bs->prefetch <<= _bs->offset; \
        } \
    } \
} while(0)

#define BitStream_Write_Bits(_bs, _bits, _nbits) do { \
    _bs->position += _nbits; \
    _bs->offset += _nbits; \
    if (_bs->offset < 32) { \
        _bs->accumulator |= (_bits << (32 - _bs->offset)); \
    } else { \
        _bs->offset -= 32; \
        _bs->mask = ((1 << (_nbits - _bs->offset)) - 1); \
        _bs->accumulator |= ((_bits >> _bs->offset) & _bs->mask); \
        BitStream_Flush(bs); \
        _bs->accumulator = 0; \
        _bs->pointer += 4; \
        if (_bs->offset) { \
            _bs->mask = ((1 << _bs->offset) - 1); \
            _bs->accumulator |= ((_bits & _bs->mask) << (32 - _bs->offset)); \
        } \
    } \
} while(0)

void BitStream_Attach(wBitStream* bs, BYTE* buffer, UINT32 capacity)
{
    bs->position = 0;
    bs->buffer = buffer;
    bs->offset = 0;
    bs->accumulator = 0;
    bs->pointer = bs->buffer;
    bs->capacity = capacity;
    bs->length = bs->capacity * 8;
}

wBitStream* BitStream_New()
{
    wBitStream* bs = NULL;

    bs = (wBitStream*) calloc(1, sizeof(wBitStream));

    return bs;
}

void BitStream_Free(wBitStream* bs)
{
    free(bs);
}
于 2014-06-18T19:11:49.017 回答
1

唔。不幸的是,我要指出的不一定是有效的实现,我希望有人发布更好的答案(我没有像 Multimedia Mike 建议的那样检查 ffmpeg)。但是我可以在您的理解中看到足够多的漏洞,让我认为值得将其发布为答案。

首先,宏不是良好性能所必需的!这是一种非常过时的方法,除非您正在为非常古老或不成熟的编译器进行编码。内联函数与宏一样有效(有一些警告,有时会提高效率)。通过明智地使用静态函数,编译器可以决定什么应该内联,什么不应该。虽然 gcc 可能不是一个优秀的编译器,但它擅长确定值何时为常量,甚至是指针!这也消除了将常量放入宏的需要。也就是说,这个:

#define UINT_BIT_SIZE (sizeof(uint) * 8)

static const size_t UINT_BIT_SIZE = sizeof(uint) * 8;

除了后者有一个类型。对于最新版本的 gcc(过去 4 年左右),您甚至不需要const将其视为编译时常量,只要它被标记为静态(或本地)并且值没有被修改编译单元中的任何代码都将其视为编译时常量。

CPU 缓存的出现从根本上改变了使一段代码变快或(相对)变慢的原因。如果您的代码的热部分不适合上层缓存(L1 / L2),那么您的内联和/或宏最终会减慢您的速度,尤其是在您必须访问主内存时。同样,同时在多个位置接触数据会导致大量缓存未命中。

话虽如此,我写了“Bit Creek”,这是 Linux 设备驱动程序的非性能关键部分的次要实现(“creek”就像“不是一个流”中的“creek”)。struct bit_creek表示一段内存,您将其视为位流和函数,creek_get_bits并将creek_put_bits其读取或写入作为返回的流-EOVERFLOW在缓冲区溢出时

如果我将 skip_bits 存储在结构中,我可能会挤出更多的性能,甚至,正如你所建议的,在我有一个完整的字节之前,甚至不要将字节写入主存储器,但性能对此并不重要。

祝你好运!

于 2014-06-09T20:48:44.843 回答