我写了下面提到的代码。代码检查每个字节的第一位。如果每个字节的第一位等于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拆包的形式。答案 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 解码和编码的运行时成本。