開發那些事--不要過度依賴snprintf/sprintf
開發那些事–不要過度依賴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
對於DATETIME型別,其實只是需要輸出xxxx-mm-dd hh-mm–ss.uuuuuu格式的資料。要輸出的位數已經確定,可以使用更簡單的方式例如兩位的moth/day,或者可能3位的數字:
//should guarantee buff have two bytes //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 (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雖然用起來方便,但一定要分析好使用場景和功能,防止出現效能問題或者正確性問題。