integer string极速互转 优化过程

来源:互联网 发布:战地4网络不好会掉帧吗 编辑:程序博客网 时间:2024/06/06 04:19

【前言】

在C/C++编码中有大量的string、interger互转需求,系统接口要不好用,要么性能不高。基于性能优化、个人兴趣两个主要目的,对string、integer互转优化做了大量的尝试,下面分享一下优化中的一些过程。


【优化效果概览


32位环境(-O2):

[----------] 4 tests from performance

[ RUN ] performance.int32_to_str

fi2s int32 cost: 16765 us

snprintf int32 cost: 594351 us

!!!!!!!!!!!! fi2s(32) is 35.451894 times faster than snprintf

[ OK ] performance.int32_to_str (613 ms)

[ RUN ] performance.int64_to_str

fi2s int64 cost: 189455 us

snprintf int64 cost: 1625197 us

!!!!!!!!!!!! fi2s(64) is 8.578275 times faster than snprintf

[ OK ] performance.int64_to_str (1815 ms)

[ RUN ] performance.str_to_int32

fs2i int32 cost: 3763 us

atoi int32 cost: 77420 us

!!!!!!!!!!!! fs2i(32) is 20.574010 times faster than atoi

[ OK ] performance.str_to_int32 (81 ms)

[ RUN ] performance.str_to_int64

fs2i int64 cost: 5521 us

atoi int64 cost: 168755 us

!!!!!!!!!!!! fs2i(64) is 30.566021 times faster than atoll

[ OK ] performance.str_to_int64 (174 ms)

[----------] 4 tests from performance (2683 ms total)

64位环境(-O2):

[----------] 4 tests from performance

[ RUN ] performance.int32_to_str

fi2s int32 cost: 20086 us

snprintf int32 cost: 502266 us

!!!!!!!!!!!! fi2s(32) is 25.005775 times faster than snprintf

[ OK ] performance.int32_to_str (522 ms)

[ RUN ] performance.int64_to_str

fi2s int64 cost: 80195 us

snprintf int64 cost: 1261576 us

!!!!!!!!!!!! fi2s(64) is 15.731355 times faster than snprintf

[ OK ] performance.int64_to_str (1342 ms)

[ RUN ] performance.str_to_int32

fs2i int32 cost: 4471 us

atoi int32 cost: 67413 us

!!!!!!!!!!!! fs2i(32) is 15.077835 times faster than atoi

[ OK ] performance.str_to_int32 (72 ms)

[ RUN ] performance.str_to_int64

fs2i int64 cost: 5735 us

atoi int64 cost: 115071 us

!!!!!!!!!!!! fs2i(64) is 20.064690 times faster than atoll

[ OK ] performance.str_to_int64 (121 ms)

[----------] 4 tests from performance (2057 ms total)

【整数转字符串优化】

一般用系统接口snprintf(sprintf是高危函数,不考虑)来做整数转字符串的需求,用起来稍微有点麻烦,大致如下:

char buffer[32];

snprintf(buffer, sizeof(buffer), "%d", 12345);

考虑到snprintf性能不好、不方便,决定实现一个自己的转换。

常规的转换(这里以正整数举例,负数需要先处理符号):

char buffer[32] = {0};

char* ptr = buffer + 30; // pos 31 for tail '\0'

int n = 12345, d = 0;

do

{

d = n / 10;

*(ptr--) = n - d * 10 + '0'; // 乘法+减法 代替求模

n = d;

} while (n != 0);

最终ptr指向的字符串就是我们想要的结果。

由上面代码可见,常规的方法需要作大量除法,int最多需要除10次,int64最多需要除20次,而除法操作需要大量的时钟周期,所以想优化的话,就要尽量减少除法的次数。

这里整数转字符串的核心思想很简单:用空间换时间,减少除法次数

先建立一个字典: 

struct HydraIntStrNode

{

char str[5];

char len;

};

HydraIntStrNode g_hydra_int_to_str[10^5]; // 数字到串的映射

建立整数到字符串的映射,数组的下标是整数,对应的值就是字符串。使得【0,99999】这个区间的的整数转字符串可以直接查找字典(数组)完成。

那么为什么我们这里字典的元素要设置成10^5了,为什么不设置成2^32,直接将所有int映射成串不是更快?原因很简单,2^32*6字节=24G,浪费不起这么多内存。

而设置成10^5只需要几百k,而且uint最多10位,转换也最多需要一次除法!10^5是一个很好的折衷点。

转换逻辑(这里针对正整数,负数可以先处理符号,再转换成正整数):

如果 整数 < 10^5

直接从字典里面取映射串

否则如果 整数 < 10^10 // 需要一次除法

将整数/10^5得到high+low两截,high、low一定都在【0,99999】这个区间里面,分别从字典里面取得映射串然后作拼接即可

否则如果 整数 < 10^15 // 需要两次除法

分成3截,从字典取出3个串进行拼接

否则 // 肯定 < 10^20, 因为uint64最大值 < 10^20,需要3次除法

分成4截,从字典取出4个串进行拼接

以上转换逻辑非常简单,但是为了性能,实际操作起来比这个逻辑要复杂。考虑到int64运算比int32运算要慢很多,

所以实际上【10^10, 10^15】这个区间被一分为二:【10^10,2^32-1】,【2^32,10^15】,使得落在【10^10,2^32-1】这个区间的整数可以直接用uint运算,从而得到更高的性能。

另外值得一提的是“前缀0填充”,对与类似100000这样的数字,会做一次除法,导致数字被截断成两截:1, 00000。00000这一截实际上在uint里面存储的只是0,

从字典得到的映射串是“0”,而不是我们预期的“00000”。对于这种情况需要进行”前缀0填充“。在前缀0填充的时候又有一个值得注意的细节,用for循环填充?用memset?答案都是NO,这两种填充都太慢,好的方法是:*(uint64*)dst = *(uint64*)"00000000"; *(uint*)dst = *(uint*)"0000"; 聪明的你一看就懂了,不用多讲,对于填充的字节不是8也不是4的情况,需要做一些修补工作,详情请参考代码。

最后一个小技巧,将字典的构造放在具有__attribute__((constructor)) 属性的函数里面完成,不需要任何显式的调用,程序加载的时候自动调用它,非常方便。



字符串转整数优化

这其中的版本有5个以上,这里只列出主要的几个

第一版:

最朴素的迭代乘、最多比ato*快3倍

代码:

template

TInt StrToIntImpl(const char* str)

{

TInt res = 0;

for (char c; (c = *(str++)) >= '0' && c <= '9'; )

res = res * 10 + (c ^ '0');

return res;

}

第二版:

在第一版基础上做循环展开、比第一版快,最多比ato*快5倍

代码:

#define _TO_NUM(v) ((v) ^ '0')

#define _IS_NUM(v) (v >= '0' && v <= '9')


template

TInt StrToIntImpl(const char* str)

{

TInt res = 0;

for (int k = 0; ; k += 4)

{

if (not _IS_NUM(str[k]))

return res;

else if (not _IS_NUM(str[k + 1]))

return res * 10 + _TO_NUM(str[k]);

else if (not _IS_NUM(str[k + 2]))

return res * 100 + _TO_NUM(str[k]) * 10 + _TO_NUM(str[k + 1]);

else if (not _IS_NUM(str[k + 3]))

{

return res * 1000 + _TO_NUM(str[k]) * 100

+ _TO_NUM(str[k + 1]) * 10 + _TO_NUM(str[k + 2]);

}

else 

{

res = res * 10000 + _TO_NUM(str[k]) * 1000 + _TO_NUM(str[k + 1]) 
                * 
 100 + _TO_NUM(str[k + 2]) * 10 + _TO_NUM(str[k + 3]);

}

}

return res;

}

第三版:

乘法相对耗时,试图减少乘法次数。方法是先建立一个字典,使得每四个字节才需要一次乘法。

字典定义为一个数组:

ushort dict[65536]; 

映射策略:将形如“1234567”这样的串切分成“1234 567”这样的串,用位运算将“1234”拼凑成一个整数,拼凑成的整数只需要占用2个字节。

拼凑策略:因为最大的单个数字9(二进制布局:1111,也就是8+0+0+1)只需要占用4个比特位,所以可以将2个数字字符“紧凑”到一个字节里面,

4个数字字符经过 移位、或 运算后,可以“紧凑”到2个字节(unsigned short)里面,从而可以讲拼凑成的unsigned short数字作为字典的下标,

找到对应的值(dict【紧凑("1234”)】=1234)。

这样划分后,实际上将10进制转换成了10000进制,所以每次迭代需要 乘10000,而不再是10. 

代码:

#define _IS_NUM(v) (v >= '0' && v <= '9')

#define _TO_NUM(v) ((v) ^ '0')

#define _SHIFT(idx, n) (_TO_NUM(str[idx]) << n)

#define _4BYTE (_SHIFT(k,12) | _SHIFT(k+1,8) | _SHIFT(k+2,4) | _SHIFT(k+3,0))

#define _3BYTE (_SHIFT(k,8) | _SHIFT(k+1,4) | _SHIFT(k+2,0))

#define _2BYTE (_SHIFT(k,4) | _SHIFT(k+1,0))

#define _1BYTE (_SHIFT(k,0))


template

TInt StrToIntImpl(const char* str)

{

TInt res = 0;

int len = 0, k = 0;

while (_IS_NUM(str[len])) ++len;

if (0 == len) return 0;

static const void* ADDR1[] = { &&a0, &&a1, &&a2, &&a3, &&a4, &&a5, };

static const void* ADDR2[] = { &&aa0, &&aa1, &&aa2, &&aa3, };

static const uint MUL[] = { 1, 10, 100, 1000, };

goto *ADDR1[len >> 2];

a5:

res = (res + g_hydra_str_to_int[_4BYTE]) * 10000; k += 4;

a4:

res = (res + g_hydra_str_to_int[_4BYTE]) * 10000; k += 4;

a3:

res = (res + g_hydra_str_to_int[_4BYTE]) * 10000; k += 4;

a2:

res = (res + g_hydra_str_to_int[_4BYTE]) * 10000; k += 4;

a1:

res = (res + g_hydra_str_to_int[_4BYTE]) * MUL[len & 3]; k += 4;

a0:

goto *ADDR2[len & 3];

aa3:

return res + g_hydra_str_to_int[_3BYTE];

aa2:

return res + g_hydra_str_to_int[_2BYTE];

aa1:

return res + g_hydra_str_to_int[_1BYTE];

aa0:

return res;

}

遗憾的是,这个版本居然比最朴素的版本还要慢!让人非常失望。感觉是某个小细节没有做对,不该这样慢的!

第四版:

前面的版本都比较失败,是因为无论如何,都没有摆脱乘法操作,如果有办法完全摆脱乘法,岂不是非常完美?

。。。。各种头脑风暴。。。。。,最后想到一种方式: 首先依然依赖字典,不过这个字典跟之前的有所不同,严格

来说,不是一个字典,而是N个。举例如下:

第1个字典,映射 单个数字字符 到 数字*10^1(10的一次方,下同), 如map1['8'] = 8 * 10^1, map1['5'] = 5 * 10^1.

第2个字典,映射 单个数字字符 到 数字*10^2, 如map2['8'] = 8 * 10^2, map2['5'] = 5 * 10^2.

。。。。。

第19个字典,映射 单个数字字符 到 数字*10^19, 如map19['1'] = 1 * 10^19, map19['5'] = 5 * 10^19(实际上5*10^19已经超出了uint64的范围,实际用不到,忽略).

转换算法:

例子1:"123":  map2['1'] + map1['2'] + ('3' ^ '0') => 123(integer)

例子2:"1234567":  map6['1'] + map5['2]' + map4['3]' + map3['4]' + map2['5]' + map1['6]' + ('7' ^ '0') => 1234567(integer)

以此类推。。

代码:

#define HYDRA_TO_NUM(v) (v ^ '0')

#define HYDRA_IS_NUM(v) (v >= '0' && v <= '9')


static inline uint64 StrToIntImpl(const char* in)

{

const uchar* str = (const uchar*)in;

// 整数最多20字节(参照ULLONG_MAX)

if (not HYDRA_IS_NUM(str[0]))

return 0;

else if (not HYDRA_IS_NUM(str[1]))

return HYDRA_TO_NUM(str[0]);

else if (not HYDRA_IS_NUM(str[2]))

return g_hydra_mul1[str[0]] + HYDRA_TO_NUM(str[1]);

else if (not HYDRA_IS_NUM(str[3]))

return g_hydra_mul2[str[0]] + g_hydra_mul1[str[1]] 

    + HYDRA_TO_NUM(str[2]);

else if (not HYDRA_IS_NUM(str[4]))

{

uint a = g_hydra_mul3[str[0]] + g_hydra_mul2[str[1]];

uint b = g_hydra_mul1[str[2]] + HYDRA_TO_NUM(str[3]);

return a + b;

}

else if (not HYDRA_IS_NUM(str[5]))

{

uint a = g_hydra_mul4[str[0]] + g_hydra_mul3[str[1]];

uint b = g_hydra_mul2[str[2]] + g_hydra_mul1[str[3]];

return a + b + HYDRA_TO_NUM(str[4]);

}

else if (not HYDRA_IS_NUM(str[6]))

{

uint a = g_hydra_mul5[str[0]] + g_hydra_mul4[str[1]];

uint b = g_hydra_mul3[str[2]] + g_hydra_mul2[str[3]];

uint c = g_hydra_mul1[str[4]] + HYDRA_TO_NUM(str[5]);

return a + b + c;

}

// .... 省略N行

else if (not HYDRA_IS_NUM(str[20]))

{

uint64 a = g_hydra_mul19[str[0]] + g_hydra_mul18[str[1]];

uint64 b = g_hydra_mul17[str[2]] + g_hydra_mul16[str[3]];

uint64 c = g_hydra_mul15[str[4]] + g_hydra_mul14[str[5]];

uint64 d = g_hydra_mul13[str[6]] + g_hydra_mul12[str[7]];

uint64 e = g_hydra_mul11[str[8]] + g_hydra_mul10[str[9]];

uint64 f = g_hydra_mul9[str[10]] + g_hydra_mul8[str[11]];

uint g = g_hydra_mul7[str[12]] + g_hydra_mul6[str[13]];

uint h = g_hydra_mul5[str[14]] + g_hydra_mul4[str[15]];

uint i = g_hydra_mul3[str[16]] + g_hydra_mul2[str[17]];

uint j = g_hydra_mul1[str[18]] + HYDRA_TO_NUM(str[19]);

uint64 k = a + b;

uint64 l = c + d;

uint64 m = e + f;

uint n = g + h;

uint o = i + j;

return k + l + m + n + o;

}

else

return 0;

}

特点:

1. 乘法零使用,查找字典做加法和位运算

2. 完全循环展开,编译器可以充分优化

3. 无关联部位“并行加”将结果存储到不同的变量,最后将和汇总

结果:没让人失望,比ato*快了20到30多倍。虽然结果令人满意,但是过程依然艰辛,一开始将StrToIntImpl实现为模板,发现只比ato*快4倍,有点失望,后来发现StrToIntImpl的实现根本与模板无关,就把模板变成了内联函数,返回值统一为uint64(负数的符号处理等更多细节这里略过,可参照完整代码),速度提升的令人意外!应该是跟编译的细节有关联,暂时没有深究。后来想想这个实现有点长,试图把实现放到cpp里面,测试发现性能大打折扣,只比ato*快4倍,所以决定保留为inline实现。

后话:也许这并不是最佳性能,如果用SIMD向量指令并行计算的话,可以带来二次惊喜吗?优化无止境!