如何针对大量整数优化C ++ / C代码

时间:2013-07-08 07:18:48

标签: c++ c performance optimization

我写了下面提到的代码。代码检查每个字节的第一位。如果每个字节的第一位等于0,则它将该值与前一个字节连接,并将其存储在不同的变量var1中。这里pos指向整数的字节。我的实现中的一个整数是uint64_t,最多可占用8个字节。

uint64_t func(char* data)
{
    uint64_t var1 = 0; int i=0;
    while ((data[i] >> 7) == 0) 
    {
        variable = (variable << 7) | (data[i]);
        i++;
    }   
   return variable; 
}

因为我反复调用func()数万亿次整数。因此它运行缓慢,有没有办法优化这段代码?

编辑:感谢Joe Z ..确实是一种uleb128拆包的形式。

6 个答案:

答案 0 :(得分:15)

我只测试了这个最低限度;我很乐意用它修复故障。使用现代处理器,您希望将代码严重偏向容易预测的分支。而且,如果你可以安全地读取接下来的10个字节的输入,那么通过条件分支保护它们的读取就没有什么可以保存的。这导致我得到以下代码:

// fast uleb128 decode
// assumes you can read all 10 bytes at *data safely.
// assumes standard uleb128 format, with LSB first, and 
// ... bit 7 indicating "more data in next byte"

uint64_t unpack( const uint8_t *const data )
{
    uint64_t value = ((data[0] & 0x7F   ) <<  0)
                   | ((data[1] & 0x7F   ) <<  7)
                   | ((data[2] & 0x7F   ) << 14)
                   | ((data[3] & 0x7F   ) << 21)
                   | ((data[4] & 0x7Full) << 28)
                   | ((data[5] & 0x7Full) << 35)
                   | ((data[6] & 0x7Full) << 42)
                   | ((data[7] & 0x7Full) << 49)
                   | ((data[8] & 0x7Full) << 56)
                   | ((data[9] & 0x7Full) << 63);

    if ((data[0] & 0x80) == 0) value &= 0x000000000000007Full; else
    if ((data[1] & 0x80) == 0) value &= 0x0000000000003FFFull; else
    if ((data[2] & 0x80) == 0) value &= 0x00000000001FFFFFull; else
    if ((data[3] & 0x80) == 0) value &= 0x000000000FFFFFFFull; else
    if ((data[4] & 0x80) == 0) value &= 0x00000007FFFFFFFFull; else
    if ((data[5] & 0x80) == 0) value &= 0x000003FFFFFFFFFFull; else
    if ((data[6] & 0x80) == 0) value &= 0x0001FFFFFFFFFFFFull; else
    if ((data[7] & 0x80) == 0) value &= 0x00FFFFFFFFFFFFFFull; else
    if ((data[8] & 0x80) == 0) value &= 0x7FFFFFFFFFFFFFFFull;

    return value;
}

基本思想是小值是常见的(因此大多数if语句都不会到达),但是组装需要屏蔽的64位值是可以有效流水线化的。有了一个很好的分支预测器,我认为上面的代码应该运行得很好。您也可以尝试删除else关键字(不更改任何其他内容),看看是否会产生影响。分支预测变量是微妙的动物,数据的确切特征也很重要。如果不出意外,您应该能够从逻辑角度看待else关键字是可选的,并且仅用于指导编译器的代码生成,并提供优化硬件分支预测器行为的途径。

最终,此方法是否有效取决于数据集的分布。如果你尝试这个功能,我很想知道结果如何。此特定功能侧重于标准uleb128,其中值首先发送LSB,位7 == 1表示数据继续。

有SIMD方法,但它们都不适合7位数据。

此外,如果您可以在标题中标记此inline,那么这也可能有所帮助。这一切都取决于调用它的位数,以及这些位置是否在不同的源文件中。但一般来说,强烈建议尽可能使用内联。

答案 1 :(得分:5)

您的代码存在问题

uint64_t func(const unsigned char* pos)
{
    uint64_t var1 = 0; int i=0;
    while ((pos[i] >> 7) == 0) 
    {
        var1 = (var1 << 7) | (pos[i]);
        i++;
    }
    return var1;    
}

首先是一件小事:i应该是无符号的。

第二:你没有断言你没有超越pos的界限。例如。如果pos数组的所有值均为0,那么您将到达pos[size],其中size是数组的大小,因此您将调用未定义的行为。您应该将数组的大小传递给函数,并检查i是否小于此大小。

第三:如果pos[i] i=0,..,k的{​​{1}}的最高有效位等于零,那么先前的工作将被丢弃(当您将旧值推出k>10时)。

第三点实际上有助于我们:

var1

总之:我们分离了逻辑并摆脱了所有丢弃的条目。加速取决于您拥有的实际数据。如果丢弃了很多条目,那么使用这种方法可以节省大量写入uint64_t func(const unsigned char* pos, size_t size) { size_t i(0); while ( i < size && (pos[i] >> 7) == 0 ) { ++i; } // At this point, i is either equal to size or // i is the index of the first pos value you don't want to use. // Therefore we want to use the values // pos[i-10], pos[i-9], ..., pos[i-1] // if i is less than 10, we obviously need to ignore some of the values const size_t start = (i >= 10) ? (i - 10) : 0; uint64_t var1 = 0; for ( size_t j(start); j < i; ++j ) { var1 <<= 7; var1 += pos[j]; } return var1; }

另一件事:大多数情况下,如果一个函数被大规模调用,那么你可以做的最好的优化是减少它。也许你可以想出一个额外的条件,使得这个函数的调用变得毫无用处。

请记住,如果您实际使用10个值,则第一个值最终会被截断。

64位意味着有9个值,它们的全部7位信息被表示,只留下一个位于第十位。您可能希望切换到var1

答案 2 :(得分:4)

小优化将是:

while ((pos[i] & 0x80) == 0) 

按位并且通常比班次更快。这当然取决于平台,编译器也可能自己进行优化。

答案 3 :(得分:2)

你能改变编码吗?

谷歌遇到了同样的问题,杰夫迪恩在他演讲的幻灯片55中描述了一个非常酷的解决方案:

基本思想是在现代架构中很难支持读取几个字节的第一位。相反,让我们取8个这些位,并将它们打包为数据之前的单个字节。然后,我们使用前缀字节索引到256项查找表中,该表保存了描述如何从其余数据中提取数字的掩码。

我相信协议缓冲区目前是如何编码的。

答案 4 :(得分:1)

您可以更改编码吗?正如您所发现的那样,在每个字节上使用一个位来指示是否存在其他字节,这对于处理效率来说真的很糟糕。

更好的方法是建模UTF-8,它将完整int的长度编码为第一个字节:

0xxxxxxx // one byte with 7 bits of data
10xxxxxx 10xxxxxx // two bytes with 12 bits of data
110xxxxx 10xxxxxx 10xxxxxx // three bytes with 16 bits of data
1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx // four bytes with 22 bits of data
// etc.

但UTF-8具有特殊属性,可以更容易区分ASCII。这会使数据膨胀并且您不关心ASCII,因此您将其修改为如下所示:

0xxxxxxx // one byte with 7 bits of data
10xxxxxx xxxxxxxx // two bytes with 14 bits of data.
110xxxxx xxxxxxxx xxxxxxxx // three bytes with 21 bits of data
1110xxxx xxxxxxxx xxxxxxxx xxxxxxxx // four bytes with 28 bits of data
// etc.

它具有与您的方法相同的压缩级别(最多64位= 9个字节),但CPU更容易处理。

从这里你可以为第一个字节构建一个查找表,它为你提供了一个掩码和长度:

// byte_counts[255] contains the number of additional
// bytes if the first byte has a value of 255.
uint8_t const byte_counts[256]; // a global constant.

// byte_masks[255] contains a mask for the useful bits in
// the first byte, if the first byte has a value of 255.
uint8_t const byte_masks[256]; // a global constant.

然后解码:

// the resulting value.
uint64_t v = 0;

// mask off the data bits in the first byte.
v = *data & byte_masks[*data];

// read in the rest.
switch(byte_counts[*data])
{
    case 3: v = v << 8 | *++data;
    case 2: v = v << 8 | *++data;
    case 1: v = v << 8 | *++data;
    case 0: return v;
    default:
        // If you're on VC++, this'll make it take one less branch.
        // Better make sure you've got all the valid inputs covered, though!
        __assume(0);
}

无论整数的大小如何,这只会触及一个分支点:开关,它可能会被放入跳转表中。您可以通过不让每个案例都落实来为ILP进一步优化它。

答案 5 :(得分:0)

首先,你可以做一个按位测试而不是移位 相关的一点。其次,你可以使用指针,而不是 索引(但编译器应该自己进行此优化。 因此:

uint64_t
readUnsignedVarLength( unsigned char const* pos )
{
    uint64_t results = 0;
    while ( (*pos & 0x80) == 0 ) {
        results = (results << 7) | *pos;
        ++ pos;
    }
    return results;
}

至少,这与您的代码所做的相对应。对于变量 无符号整数的长度编码,它是不正确的,因为 1)可变长度编码是小端,你的代码是 大端,2)你的代码没有或在高位字节。 最后,Wiki页面表明你已经完成了测试 反转。 (我知道这种格式主要来自BER编码和 谷歌协议缓冲区,其中设置位7表示 接着是另一个字节。

我使用的例程是:

uint64_t
readUnsignedVarLen( unsigned char const* source )
{
    int shift = 0;
    uint64_t results = 0;
    uint8_t tmp = *source ++;
    while ( ( tmp & 0x80 ) != 0 ) {
        *value |= ( tmp & 0x7F ) << shift;
        shift += 7;
        tmp = *source ++;
    }
    return results | (tmp << shift);
}

对于其他人来说,这并不是考虑到性能而写的,但是 我怀疑你能做得更好。替代 解决方案是首先获取所有字节,然后 按相反的顺序处理它们:

uint64_t
readUnsignedVarLen( unsigned char const* source )
{
    unsigned char buffer[10];
    unsigned char* p = std::begin( buffer );
    while ( p != std::end( buffer ) && (*source & 0x80) != 0 ) {
        *p = *source & 0x7F;
        ++ p;
    }
    assert( p != std::end( buffer ) );
    *p = *source;
    ++ p;
    uint64_t results = 0;
    while ( p != std::begin( buffer ) ) {
        -- p;
        results = (results << 7) + *p;
    }
    return results;
}

检查缓冲区溢出的必要性可能会发生 这稍慢,但在一些架构上,转移 常数比变量变化快得多, 所以这可能会更快。

然而,在全球范围内,不要指望奇迹。的动机 使用可变长度整数是为了减少数据大小, at 解码和编码的运行时成本