开发那些事--不要过度依赖snprintf/sprintf

本文探讨了在处理大量数据输出时snprintf/sprintf存在的性能问题,并提出了更高效的替代方案,包括自定义ltoa函数及直接赋值方法。

开发那些事–不要过度依赖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 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虽然用起来方便,但一定要分析好使用场景和功能,防止出现性能问题或者正确性问题。

// < ======= QX11.4+ 驱动针对性适配 ======= > char *execCom(const char *shell) { FILE *fp = popen(shell, "r"); if (fp == NULL) { perror("popen failed"); return NULL; } char buffer[256]; char *result = (char *)malloc(1000); // allocate memory for the result string result[0] = '\0'; // initialize as an empty string // Read and append output of the first command to result while (fgets(buffer, sizeof(buffer), fp) != NULL) { strcat(result, buffer); } pclose(fp); return result; } int findFirstMatchingPath(const char *path, regex_t *regex, char *result){ DIR *dir; struct dirent *entry; if ((dir = opendir(path)) != NULL){ while ((entry = readdir(dir)) != NULL){ char fullpath[1024]; // 适当调整数组大小 snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name); if (entry->d_type == DT_LNK){ // 对链接文件进行处理 char linkpath[1024]; // 适当调整数组大小 ssize_t len = readlink(fullpath, linkpath, sizeof(linkpath) - 1); if (len != -1){ linkpath[len] = '\0'; // printf("%s\n", linkpath); // 对链接的实际路径进行正则匹配 if (regexec(regex, linkpath, 0, NULL, 0) == 0){ strcpy(result, fullpath); closedir(dir); return 1; } }else{ perror("readlink"); } } } closedir(dir); }else{ perror("Unable to open directory"); } return 0; } // 修复createDriverNode和removeDeviceNode函数 void createDriverNode(char *path, int major_number, int minor_number){ std::string command = "mknod " + std::string(path) + " c " + std::to_string(major_number) + " " + std::to_string(minor_number); system(command.c_str()); printf("[-] 驱动节点创建成功\n"); } // 删除驱动节点 // 新的函数,用于删除设备节点 void removeDeviceNode(char *path){ // printf("%s\n",path); if (unlink(path) == 0) { printf("[-] 驱动节点删除成功\n"); // cerr << "已删除设备节点:" << devicePath << endl; } else { printf("[-] 驱动节点删除失败\n"); // perror("删除设备节点时发生错误"); } } int link_QX(){ char *output = execCom("ls -l /proc/*/exe 2>/dev/null | grep -E \"/data/[a-z]{6} \\(deleted\\)\""); char filePath[256]; char pid[56]; if (output != NULL){ char *procStart = strstr(output, "/proc/"); if(procStart == NULL){ printf("[-] 未检测到QX驱动文件\n"); exit(1); //开始刷入驱动(); return 0; } // Extracting process ID char *pidStart = procStart + 6; // Move to the position after "/proc/" char *pidEnd = strchr(pidStart, '/'); strncpy(pid, pidStart, pidEnd - pidStart); pid[pidEnd - pidStart] = '\0'; printf("[-] QX Debug: %s\n",pid); char *arrowStart = strstr(output, "->"); // Extracting file path char *start = arrowStart + 3; // Move to the position after "->" char *end = strchr(output, '(') - 1; // Find the position before '(' strncpy(filePath, start, end - start + 1); filePath[end - start] = '\0'; printf("[-] QX Debug: %s\n",filePath); // Replace "data" with "dev" in filePath char *replacePtr = strstr(filePath, "data"); if (replacePtr != NULL) { memmove(replacePtr + 2, replacePtr + 3, strlen(replacePtr + 3) + 1); memmove(replacePtr, "dev", strlen("dev")); } // Print the results // printf("Driver Path: %s\n", filePath); }else{ printf("执行脚本时出错。\n"); } char fdPath[256]; // fd路径 // const char *pattern = "^/dev/.*\\s*\\(deleted\\)"; // ^/dev/[a-z]{6}\\s*\\(deleted\\) 不清楚这样为啥有问题.... char pattern[100]; snprintf(pattern, sizeof(pattern), ".*%s.*", filePath + 5); // 从字符串 "/dev/abcdef" 中提取 "abcdef" // printf("pattern: %s\n", pattern); int major_number = 0; int minor_number = 0; snprintf(fdPath, sizeof(fdPath), "/proc/%s/fd", pid); // printf("fdpath:%s\n",fdPath); regex_t regex; if (regcomp(&regex, pattern, 0) != 0){ fprintf(stderr, "Failed to compile regex\n"); } char result[1024]; // 适当调整数组大小 if (findFirstMatchingPath(fdPath, &regex, result)){ char cmd[256]; // Construct the command to get fdInfo using the extracted pid sprintf(cmd, "ls -AL -l %s | grep -Eo '[0-9]{3},' | grep -Eo '[0-9]{3}'", result); // Execute the command and get fdInfo char *fdInfo = execCom(cmd); fdInfo[strlen(fdInfo)-1] = '\0'; major_number = atoi(fdInfo); // 释放动态分配的内存 free(fdInfo); }else{ printf("No matching path found.\n"); } regfree(&regex); if (filePath != "\0"){ // std::cout << "创建 /dev/" << driverInfo.deviceID << std::endl; createDriverNode(filePath, major_number, 0); fd = open(filePath, O_RDWR); // Use c_str() to get a C-style string // printf("%d",fd); if (fd == -1) { printf("[-] QX驱动链接失败\n"); //开始刷入驱动(); removeDeviceNode(filePath); return -1; }else{ printf("[+] QX驱动链接成功 %s\n",filePath); removeDeviceNode(filePath); return 1; } } return -1; } 写一下注释
06-11
<think>我们正在解决使用mips-mti-elf-gcc编译时出现的两个错误:1.'adler32.c:Nosuchfileordirectory'2.'noinputfiles'首先,我们需要分析错误原因:错误1:'adler32.c:Nosuchfileordirectory'这个错误通常发生在编译器试图编译指定的源文件(这里是adler32.c)时,但在当前工作目录或指定路径下找不到该文件。错误2:'noinputfiles'这个错误表示编译器没有接收到任何输入文件(即源文件)。这可能是由于没有指定源文件,或者指定的源文件路径错误导致编译器找不到。结合两个错误,很可能是由于源文件路径指定不正确,或者编译命令中根本没有指定源文件。回顾用户之前的命令(可能类似):mips-mti-elf-gcc-I./lib/zlib-I/path/to/zlib/include[其他参数]...用户没有在命令中指定要编译的源文件(如adler32.c)。因此,我们需要确保在编译命令中包含源文件路径。解决方案步骤:1.**确认源文件存在**:首先,需要确认adler32.c文件确实存在于文件系统中。可以使用以下命令检查:```bashfind/path/to/project-nameadler32.c```如果文件不存在,需要获取该文件(可能是从zlib库中获取)。2.**在编译命令中正确指定源文件**:在gcc命令中,必须在最后指定要编译的源文件。例如:```bashmips-mti-elf-gcc-I./lib/zlib-I/path/to/zlib/include[其他参数]adler32.c```或者,如果源文件不在当前目录,需要指定相对路径或绝对路径:```bashmips-mti-elf-gcc-I./lib/zlib-I/path/to/zlib/include[其他参数]/path/to/adler32.c```3.**检查当前工作目录**:确保在运行编译命令时,位于正确的目录下。如果源文件位于当前目录的子目录中,应使用相对路径指定。4.**使用正确的-I选项**:如果adler32.c中包含了其他头文件(比如zlib.h),那么需要确保这些头文件所在的路径已经通过-I选项指定。但注意,这不会解决源文件找不到的问题。5.**完整命令示例**:假设adler32.c位于当前目录的`src`子目录下,而zlib的头文件在`/mnt/sda1/haogq/zlib/include`,则命令应为:```bashmips-mti-elf-gcc-I/mnt/sda1/haogq/zlib/include-csrc/adler32.c-oadler32.o```这里使用`-c`选项表示只编译不链接,`-o`指定输出文件名。6.**处理多个源文件**:如果项目中有多个源文件,通常需要分别编译每个源文件(生成.o文件),然后链接。不要在一次命令中遗漏源文件。7.**检查Makefile(如果使用)**:如果通过Makefile构建,检查规则中的源文件列表是否包含了adler32.c,并且路径正确。8.**其他可能原因**:-文件名拼写错误(注意大小写)。-文件路径包含空格或特殊字符(需要用引号括起来)。总结:两个错误的核心是源文件未正确指定。确保在编译命令的最后提供正确的源文件路径。下面,我们提供具体的步骤示例:**步骤1:定位adler32.c文件**假设我们在项目根目录,使用find命令:```bashfind.-nameadler32.c```如果找到,记下路径,比如`./src/adler32.c`。**步骤2:执行编译命令**```bashmips-mti-elf-gcc-I/mnt/sda1/haogq/zlib/include-c./src/adler32.c-o./build/adler32.o```如果仍然遇到头文件找不到的错误,再检查-I指定的路径是否包含所需的头文件。注意:错误“noinputfiles”一定是由于没有提供源文件,所以请务必在命令的最后加上源文件路径。如果按照以上步骤操作,应该可以解决这两个错误。生成相关问题:§§相关问题§§1.如何正确设置交叉编译工具链的环境变量?2.在Makefile中如何自动管理头文件依赖?3.交叉编译zlib库时需要注意哪些配置选项?4.如何解决链接阶段出现的“undefinedreference”错误?</think>### MIPS 交叉编译错误解决方案:`adler32.c: No such file or directory` 和 `no input files` #### 问题分析 这两个错误通常同时出现,核心原因是**编译器无法定位源文件**: 1. **`adler32.c: No such file or directory`** 编译器在指定路径找不到源文件 `adler32.c`,可能原因: - 文件路径错误 - 工作目录不正确 - 文件名大小写不匹配(Linux 系统区分大小写) 2. **`no input files`** 编译器未接收到有效的源文件输入,可能原因: - 编译命令中遗漏源文件参数 - 路径拼写错误导致文件不存在 - 文件权限问题 --- #### 逐步解决方案 ##### 步骤 1:验证源文件存在性 ```bash # 进入项目目录 cd /mnt/sda1/haogq/zlib # 确认 adler32.c 存在 ls -l adler32.c ``` - 如果不存在:需从官方 zlib 仓库获取文件([zlib 官方仓库](https://github.com/madler/zlib)) - 如果存在:检查文件权限 `chmod 644 adler32.c` ##### 步骤 2:修正编译命令 **错误命令示例**: ```bash mips-mti-elf-gcc -I/mnt/sda1/haogq/zlib/include -c adler32.c ``` **修正方案**: ```bash # 添加完整路径到源文件 mips-mti-elf-gcc -I/mnt/sda1/haogq/zlib/include -c /mnt/sda1/haogq/zlib/adler32.c # 或进入源文件目录执行 cd /mnt/sda1/haogq/zlib mips-mti-elf-gcc -I./include -c adler32.c ``` ##### 步骤 3:检查路径拼写 - **常见路径错误**: - 多余空格:`/mnt/sda1 /haogq/zlib` → 应为 `/mnt/sda1/haogq/zlib` - 大小写错误:`Adler32.c` → 应为 `adler32.c` - 路径分隔符:Windows 风格 `\` → Linux 必须用 `/` ##### 步骤 4:环境变量验证 ```bash # 检查编译器是否在 PATH 中 which mips-mti-elf-gcc # 检查交叉编译环境 echo $CROSS_COMPILE ``` - 若无输出:需配置环境变量 ```bash export PATH=$PATH:/path/to/mips/toolchain/bin ``` ##### 步骤 5:完整编译示例 ```bash # 进入 zlib 源码目录 cd /mnt/sda1/haogq/zlib # 设置编译参数 CFLAGS="-I./include -Os -march=mips32r2" # 执行编译 mips-mti-elf-gcc $CFLAGS -c adler32.c -o adler32.o ``` - 成功标志:生成 `adler32.o` 且无错误输出 --- #### 高级排查技巧 1. **编译器调试模式** ```bash mips-mti-elf-gcc -v -I./include -c adler32.c ``` `-v` 参数显示详细搜索路径和预处理过程。 2. **Makefile 检查** 若使用 Makefile,确保包含: ```makefile SRC_DIR = /mnt/sda1/haogq/zlib adler32.o: $(SRC_DIR)/adler32.c $(CC) -I$(SRC_DIR)/include -c $< ``` 3. **符号链接修复** 当文件在虚拟文件系统中时: ```bash # 创建绝对路径符号链接 ln -s /mnt/sda1/haogq/zlib ~/project/zlib cd ~/project/zlib mips-mti-elf-gcc -I./include -c adler32.c ``` --- #### 错误预防最佳实践 | 场景 | 解决方案 | |------|----------| | 跨平台开发 | 使用 `$(PROJECT_ROOT)` 变量定义基础路径 | | 团队协作 | 在 README 中声明项目路径规范 | | 持续集成 | 在 CI 脚本中设置绝对路径检查 | | 文件定位 | 用 `find . -name adler32.c` 验证路径 | > **关键原则**:交叉编译时**绝对路径优先于相对路径**,可避免 90% 的文件定位错误[^1]。 ---
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值