开发那些事--不要过度依赖snprintf/sprintf
将数据按照指定format输出到buffer中,往往会用snprintf/sprintf(推荐前者)。但各个场景都习惯性的用snprintf/sprintf却并不是什么好事。
snprintf/sprintf性能问题
在分析团队项目性能时候,发现将大量数据以文本TEXT方式返回给客户端时,耗时非常多,且和类型有关,DATETIME > INT > varchar。perf图分析后发现,snprintf占用了很多时间。因为DATETIME调用4次,INT调用1次,varchar 0次。DATETIME和INT类型调用都主要是将INT转换为TEXT文本方式。
对于INT值打印,写ltoa函数和snprintf做性能对比。
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/time.h>
char *ltoa10(int64_t val,char *dst, const bool is_signed)
{
char buffer[65];
uint64_t uval = (uint64_t) val;
if (is_signed)
{
if (val < 0)
{
*dst++ = '-';
uval = (uint64_t)0 - uval;
}
}
register char *p = &buffer[sizeof(buffer)-1];
*p = '\0';
int64_t new_val= (int64_t) (uval / 10);
*--p = (char)('0'+ (uval - (uint64_t) new_val * 10));
val = new_val;
while (val != 0)
{
new_val=val/10;
*--p = (char)('0' + (val-new_val*10));
val= new_val;
}
while ((*dst++ = *p++) != 0) ;
return dst-1;
}
const int64_t INT_NUM = 10000;
int main()
{
//init num
int value[INT_NUM];
for (int64_t idx = 0; idx < INT_NUM; ++idx) {
value[idx] = random();
}
const int64_t MAX_CONST_LENGTH = 22;
char str[MAX_CONST_LENGTH];
struct timeval t_start, t_end;
long start, end;
//get snprintf time
gettimeofday(&t_start, NULL);
start = t_start.tv_sec * 1000000 + t_start.tv_usec;
for (int64_t idx = 0; idx < INT_NUM; ++idx) {
snprintf(str, MAX_CONST_LENGTH, "%ld", value[idx]);
}
gettimeofday(&t_end, NULL);
end = t_end.tv_sec * 1000000 + t_end.tv_usec;
printf("snprintf time:%ld\n", end - start);
//get ltoa10 time
gettimeofday(&t_start, NULL);
start = t_start.tv_sec * 1000000 + t_start.tv_usec;
for (int64_t idx = 0; idx < INT_NUM; ++idx) {
ltoa10(value[idx], str, true);
}
gettimeofday(&t_end, NULL);
end = t_end.tv_sec * 1000000 + t_end.tv_usec;
printf("ltoa time:%ld\n", end - start);
return 0;
}
O2编译执行,实验结果如下,ltoa性能是snprintf的1倍以上:
snprintf time:2053
ltoa time:833
(一篇integer to string conversion in C++各种方法比较的文章 http://zverovich.net/2013/09/07/integer-to-string-conversion-in-cplusplus.html)
对于DATETIME类型,其实只是需要输出xxxx-mm-dd hh-mm--ss.uuuuuu格式的数据。要输出的位数已经确定,可以使用更简单的方式例如两位的moth/day,或者可能3位的数字:
//should guarantee buff have two digit
//snprintf(buff, "%02d", num) num is between 0 and 100
#define PRINTF_2D_WITH_TWO_DIGIT(buff, num) \
{ \
int32_t tmp2 = (num) / 10; \
int32_t tmp = (num) - tmp2 * 10; \
*buff++ = (char) ('0' + tmp2); \
*buff++ = (char) ('0' + tmp); \
}
//snprintf(buff, "%02d", num), num is between 0 and 1000
#define PRINTF_2D_WITH_THREE_DIGIT(buff, num) \
{ \
int32_t m = (num) / 10; \
int32_t l = (num) - m * 10; \
int32_t h = m / 10; \
m = m - h * 10; \
if (h > 0) { \
*buff++ = (char) ('0' + h); \
} \
*buff++ = (char) ('0' + m); \
*buff++ = (char) ('0' + l); \
}
//deal year.year[0000-9999]
int32_t high = parts[DT_YEAR] / 100;
int32_t low = parts[DT_YEAR] - high * 100;
PRINTF_2D_WITH_TWO_DIGIT(buf_t, high);
PRINTF_2D_WITH_TWO_DIGIT(buf_t, low);
if (with_delim) {
*buf_t++ = '-';
}
上面的方式还需要做计算,如果使用200字节的字符串记录0~99的对应字符,那么对于2位数字的转换就可以直接用int64_t的赋值操作,性能对比代码如下:
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
char int_c[201] =
"000102030405060708091011121314151617181920212223242526272829303132333435363738394041424344454647484950"
"51525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899";
const int64_t INT_NUM = 10000;
int main()
{
//init data
int value[INT_NUM];
for (int64_t idx = 0; idx < INT_NUM; ++idx) {
value[idx] = random() % 100;
}
char buff[20001];
buff[20000] = '\0';
char *buf_t = buff;
struct timeval t_start, t_end;
long start, end;
//get direct assign time
gettimeofday(&t_start, NULL);
start = t_start.tv_sec * 1000000 + t_start.tv_usec;
for (int64_t idx = 0; idx < INT_NUM; ++idx) {
int val = value[idx];
*(int16_t*)buf_t = *((int16_t*)(int_c) + val);
buf_t +=2;
}
gettimeofday(&t_end, NULL);
end = t_end.tv_sec * 1000000 + t_end.tv_usec;
printf("assign time:%ld\n", end - start);
//get calc and assign time
start = t_start.tv_sec * 1000000 + t_start.tv_usec;
buf_t = buff;
for (int64_t idx = 0; idx < INT_NUM; ++idx) {
int tmp = value[idx];
int tmp2 = tmp / 10; tmp = tmp - tmp2 * 10;
*buf_t++ = (char) ('0' + tmp2);
*buf_t++ = (char) ('0' + tmp);
}
gettimeofday(&t_end, NULL);
end = t_end.tv_sec * 1000000 + t_end.tv_usec;
printf("calc and assign time:%ld\n", end - start);
}
得到性能结果,直接赋值的性能达到之前计算每个字符方式的4倍:
assign time:20
calc and assign time:85
即用下面代码做替换可以得到更好的性能。
#define PRINTF_2D_WITH_TWO_DIGIT(buff, num) \
{ \
*(int16_t*)buf = *((int16_t*)(int_c) + val); \
buf += 2; \
}
snprintf/sprintf细节理解不够
snprintf/sprint会在结尾补'0'
一个底层to_hex_str函数将输入指定data_length的in_data按字节转换为HEX值,在下面的代码中检查buff_size至少是data_length的2倍,但是sprintf会在末尾补'0',会导致内存写越界。
unsigned const char *p = NULL;
int32_t i = 0;
if (in_data != NULL && buff != NULL && buff_size >= data_length * 2) {
p = (unsigned const char *)in_data;
for (; i < data_length; i++) {
sprintf((char *)buff + i * 2, "%02X", *(p + i));
}
}
snprintf/printf %02d打印代表至少2位;%X打印char是按整数打印。
一个类似to_hex_str的代码,使用如下方式打印。
//const char* in_buf, int64_t in_len,
//char *buffer, int64_t buf_len, int64_t &pos
for (int64_t i = 0; OB_SUCC(ret) && i < in_len; ++i) {
if (OB_FAIL(databuff_printf(buffer, buf_len, pos, "%02X", *(in_buf + i)))) {
} else {}
} // end for
这里就出现一个错误,in_buf是char,是有符号的,在使用%02X打印的时候,按整数方式打印,char的范围是[-128,127),但2个16进制仅能表示[0,255]。下面的例子中,buff中得到的就是FFFFFFFF。其内容显然不是希望的。
char c = char(-1);
snprintf(buff, BUFF_SIZE, "%02X", c);
这里如果使用snprintf,应该将char*转换为unsinged char* 。
实际上,每个字节打印16进制方式可以用如下代码:
static const char *HEXCHARS = "0123456789ABCDEF";
for (int64_t i = 0; i < data_length; ++i) {
*dst++ = HEXCHARS[*in_data >> 4 & 0xF];
*dst++ = HEXCHARS[*in_data & 0xF];
in_data++;
}
snprintf/sprintf虽然用起来方便,但一定要分析好使用场景和功能,防止出现性能问题或者正确性问题。