在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向量指令做并行计算的话,可以带来二次惊喜吗?优化无止境!