读点速度更快?

时间:2012-08-07 21:30:51

标签: c++ 64-bit bit-manipulation

在我的应用程序中,20%的cpu时间花在通过我的位读取器读取位(skip)上。有没有人知道如何更快地使下面的代码?在任何给定时间,我不需要超过20个有效位(这就是为什么我在某些情况下可以使用fast_skip)。

以大端顺序读取位,这就是需要字节交换的原因。

class bit_reader
{   
    std::uint32_t* m_data;
    std::size_t    m_pos;
    std::uint64_t  m_block;

public:
    bit_reader(void* data)
        : m_data(reinterpret_cast<std::uint32_t*>(data))
        , m_pos(0)
        , m_block(_byteswap_uint64(*reinterpret_cast<std::uint64_t*>(data)))
    {
    }

    std::uint64_t value(std::size_t n_bits = 64)
    {               
        assert(m_pos + n_bits < 64);

        return (m_block << m_pos) >> (64 - n_bits);
    }

    void skip(std::size_t n_bits) // 20% cpu time
    {
        assert(m_pos + n_bits < 42);

        m_pos  += n_bits;

        if(m_pos > 31)
        {
            m_block = _byteswap_uint64(reinterpret_cast<std::uint64_t*>(++m_data)[0]);
            m_pos  -= 32;
        }
    }   

    void fast_skip(std::size_t n_bits)
    {
        assert(m_pos + n_bits < 42);
        m_pos  += n_bits;
    }   
};

目标硬件是x64。

7 个答案:

答案 0 :(得分:2)

我从之前的评论中看到,您正在解压缩JPEG中的霍夫曼/算术编码流。

  • skip()value()非常简单,可以内联。编译器有可能在整个寄存器中保留移位寄存器和缓冲区指针。使用restrict修饰符在此处和调用者中创建所有指针可能有助于告诉编译器您不会将Huffman解码的结果写入位缓冲区,从而允许进一步优化。
  • 每个霍夫曼/拟合符号的平均长度很短 - 因此,8个中的〜7倍,您不需要加满64位移位寄存器。调查给编译器一个分支预测提示。
  • JPEG比特流中的任何符号都不会超过32位。这是否允许进一步优化?
  • skip()是一条沉重路径的一个非常合乎逻辑的原因是你称之为 lot 。你一次消耗一个完整的符号而不是每一点都不是你吗?通过计算符号和表格查找中的前导0或1,可以做一些聪明的技巧。
  • 您可以考虑安排移位寄存器,使流中的下一位是LSB。这样可以避免value()
  • 的变化

答案 1 :(得分:1)

64位移位绝对不是一个好主意。在许多CPU中,移位是一个缓慢的操作。 我建议你将代码更改为字节寻址。这将限制最多8位的移位。

在许多情况下,你真的不需要一点点,而是检查它是否存在。这可以通过以下代码完成:

    if (data[bit_inx/64] & mask[bit_inx % 64])
    {
        ....
    }

答案 2 :(得分:1)

尝试在skip中替换此行:

m_block  = (m_block << 32) | _byteswap_uint32(*++m_data);

答案 3 :(得分:0)

我不知道它是什么原因以及_byteswap_uint64的底层实现是什么样的,但你应该阅读Rob Pike's article on byteorder。也许这就是你的答案。

摘要:字节顺序不像常常做的那样是一个问题。字节顺序交换的实现经常会出现问题。但是有一个简单的选择。

[编辑] 我有一个更好的理论。粘贴在我的评论下面: 也许这是别名。 64位架构喜欢将数据对齐64位,当您跨越对齐边界读取时,它会变得非常慢。因此它可能是(++m_data)[0]部分,因为x64是64位对齐的,当您reinterpret_cast uint32_t*uint64_t*时,您在大约一半的时间内跨越对齐边界。

答案 4 :(得分:0)

如果您的源缓冲区不是很大,那么您应该预先处理它们,在使用bit_reader访问它们之前对缓冲区进行字节交换!

从你的bit_reader读取会比你快得多,因为:

  1. 您将保存一些条件指示
  2. CPU缓存可以更有效地使用:它可以直接从内存中读取,这很可能已经加载到cpu缓存中,而不是从读取每个64位块后将被修改的内存中读取,因此,破坏了它的好处在缓存中使用它
  3. 修改

    等等,你不要修改源缓冲区。但是,将byteswap放入预处理阶段至少应该值得一试。

    另一点:确保那些assert()调用仅在调试版本中。

    编辑2

    (删除)

    编辑3

    您的代码肯定存在缺陷,请查看以下使用方案:

    uint32_t source[] = { 0x00112233, 0x44556677, 0x8899AABB, 0xCCDDEEFF };
    bit_reader br(source); // -> m_block = 0x7766554433221100
    // reading...
    br.value(16); // -> 0x77665544
    br.skip(16);
    br.value(16); // -> 0x33221100
    br.skip(16);  // -> triggers reading more bits
                  // -> m_block = 0xBBAA998877665544, m_pos = 0
    br.value(16); // -> 0xBBAA9988
    br.skip(16);
    br.value(16); // -> 0x77665544
    // that's not what you expect, right ???
    

    编辑4

    嗯,不,编辑3错了,但我无法帮助,代码存在缺陷。不是吗?

    uint32_t source[] = { 0x00112233, 0x44556677, 0x8899AABB, 0xCCDDEEFF };
    bit_reader br(source); // -> m_block = 0x7766554433221100
    // reading...
    br.value(16); // -> 0x7766
    br.skip(16);
    br.value(16); // -> 0x5544
    br.skip(16);  // -> triggers reading more bits (because m_pos=32, which is: m_pos>31)
                  // -> m_block = 0xBBAA998877665544, m_pos = 0
    br.value(16); // -> 0xBBAA --> not what you expect, right?
    

答案 5 :(得分:0)

这是我尝试的另一个版本,它没有提供任何性能改进。

class bit_reader
{   
public:     
    const std::uint64_t* m_data64;
    std::size_t          m_pos64;
    std::uint64_t        m_block0;
    std::uint64_t        m_block1;


    bit_reader(const void* data)
        : m_pos64(0)
        , m_data64(reinterpret_cast<const std::uint64_t*>(data))
        , m_block0(byte_swap(*m_data64++))
        , m_block1(byte_swap(*m_data64++))
    {
    }

    std::uint64_t value(std::size_t n_bits = 64)
    {               
        return __shiftleft128(m_block1, m_block0, m_pos64)  >> (64 - n_bits);
    }

    void skip(std::size_t n_bits)
    {
        m_pos64 += n_bits;

        if(m_pos64 > 63)
        {
            m_block0 = m_block1;
            m_block1 = byte_swap(*m_data64++);
            m_pos64  -= 64;
        }
    }   

    void fast_skip(std::size_t n_bits)
    {
        skip(n_bits);
    }   
};

答案 6 :(得分:-2)

如果可能,最好在多次通过中执行此操作。可以优化多次运行并减少破坏。

一般来说,最好做

const uint64_t * arr = data;

for(uint64_t * i = arr; i != &arr[len/sizeof(uint64_t)] ;i++)
{
     *i = _byteswap_uint64(*i); 
     //no more operations here
}
// another similar for loop

此类代码可以通过巨大的因素减少运行时间

最糟糕的是,你可以像100k块的运行一样,以最大限度地减少缓存未命中数,并从RAM中单独加载数据。

在你的情况下,你以流媒体的方式做到这一点,只有保持低内存和慢速数据源的更快响应,但不是速度。