C语言性能优化:3个技巧避开调用热点

张开发
2026/4/12 6:07:43 15 分钟阅读

分享文章

C语言性能优化:3个技巧避开调用热点
鉴于C语言属于最贴近汇编语言的编程语言相较于其他更为高级的编程语言而言往往运用C语言编写的程序能够获取最为出色的运行速度。然而而言恰是鉴于 C 语言存有优越的性能展现情况从而导致这般结局程序员于运用 C 语言期间常常会忽视某些低效的代码这些低效的代码于开发环境里极难被发觉可是一旦它们变成调用热点之际或许就会给程序的整体性能带来显著的作用。这地方的调用热点是指处于程序运行进程当中某一个或者某些函数会被频繁地调用也就是说在给定的统计周期之内这个或者这些函数被调用的次数远远多于其他函数。要是构成调用热点的函数恰好没有经过充分优化那么大概率会形成程序的性能瓶颈。为了防止出现这种状况于日常进行编码期间C程序员应当始终铭记下述三个技巧。为了防止去做那种没有实际效果的工作常见的那种没有实际效果的工作会出现在像下面这样的两个情景之中表示局部范围的变量特别是数组的开始阶段设置以及多出来的函数的调用。比如下面的代码void foo(void) { char buf[64] {}; memset(buf, 0, sizeof(buf)); strcpy(buf, foo); ... }之上代码最终所呈现的执行结果是把foo这个字符串复制至 buf 里。可是之上代码开展了好多回没必要的初始化操作上述两个操作从本质上来说是存在重复情况的只留下其中一个便可以了。从最终产出的汇编代码层面去看运用编程语言所给予的赋值语句来开展初始化操作其执行效率会更高一些。从这段代码最终执行之后所呈现出来的结果去看上面提及的那两个操作全部都是多余的原因在于strcpy()函数会把字符串常量foo末尾的那个空终止字符\0一起给复制到buf里面。所以上面的那段代码只需要留存如下的两行void foo(void) { char buf[64]; strcpy(buf, foo); ... }更进一步地我们可以将这段代码简化为一条赋值语句void foo(void) { char buf[64] foo; ... }按C语言语法上述语句会把字符串常量“foo”以及最后的空终止字符一同复制到buf中且不用调用strcpy()函数如此便能省下调用函数的开销。当读到此处时读者兴许会存疑问这般低效的代码于实际项目里应该不太常见吧而实际情形表明这类代码极有可能源自“具备丰富经验”的老手。探寻其缘由是他们被有可能出现的内存使用错误给吓怕了所以不管三七二十 一只要程序中涉及到数组便调用memset()进行一次重置。还有一种平常能见到的没意义的工作就是去开展一些并非必要的多余检查。下面这段代码达成了一个bar()函数#include static int bar(const char* str, size_t length) { if (str NULL) return 0; if (length 0) length strlen(str); while (*s length) { ... length--; } ... }该函数接收两个参数其中一个参数是字符串指针另一个参数是长度。从代码已有的实现方面来看length限定了该函数需要处理的最大字符个数要是给定的length为零那就意味着处理到字符串尾部。所以在while循环里既检查了*s的值又检查了length的值并且在循环的末尾执行了length--。这段代码存在无用功之处在于当我们进行测试测试到length的值是零时调用了strlen(str)函数去计算字符串的长度。实际上计算字符串的长度需要通过循环来查找空终止字符并且其后的while循环同样也要检测空终止字符所以就出现了多余的测试。想要对这一实现进行优化仅仅需要在length处于为零的情况时把SIZE_MAX宏所具有的值赋予给length就行反正while循环一直都会去判断是不是抵达了字符串的尾部那么就让其长度变为最大的那种可能的值好了。这里的SIZE_MAX宏对size_t的最大值做了定义SIZE_MAX宏是在stdint.h头文件里进行定义的。优化之后的实现是这样的#include static int bar(const char* str, size_t length) { if (str NULL) return 0; if (length 0) length SIZE_MAX; while (*s length) { ... length--; } ... }如此便可免去一次多余的 strlen() 函数调用。防止对接口进行滥用避免滥用标准C库的接口或者不去滥用某些第三方函数库的接口最为常见的情况便是对STDI接口实施滥用例如像下面所包含的两个函数调用sprintf(a_buffer, %s%s, a_string, another_string); sscanf(a_string, %d, i);存在两个函数调用其中一个实现实现的是把两个字符串进行串接的功能另一个实现的是将字符串转变为一个整型值的功能。对于在内存里执行格式化输入输出的 STDIO 接口像 sprintf() 和 sscanf() 函数这样的先是会去调用 fmemopen() 函数来构造出一个基于内存的 FILE 对象接着调用 fprintf()、fscanf() 等函数去达成最终的格式化输入输出功能最后把临时构建的 FILE 对象给销毁掉。之所以如此如此这般这样是因为呀这些接口的开销极其大不管是从空间复杂度的角度去瞧去看还是从时间复杂度的视点钻研探究都远远地大大地胜过超越 strcat()、strcpy()、atoi() 等这般函数。要是仅仅只是想要达成完成字符串的串连拼接或者单个字符串转变转换为整数的功用功能完全全然大可不必调用 STDIO 接口而是应当调用别的其他的标准库函数情况情形如下所示// sprintf(a_buffer, %s%s, a_string, another_string); strcpy(a_buffer, a_string); strcat(a_buffer, another_string); // sscanf(a_string, %d, i); i atoi(a_string);要是对于性能依旧没能满足那么能够再去对以上字符串串接代码做进一步优化。strcat() 首先会探寻到 a_buffer 的末尾部位接着复制 another_string 的内容直至该字符串的结尾之处。然而实际上strcpy() 函数在其内部的实现过程里必定是已经循环至 a_string 的末尾了所以也清楚 a_buffer 里字符串的末尾地址。可是strcpy()这个函数未曾返回a_buffer里指向字符串末尾的指针返回的反而是a_buffer自身。幸亏为了达成这一需要标准库特地添设了一个接口 stpcpy()’这个接口去复制字符串并且返回指向那头的指针#include char *stpcpy(char *dest, const char *src);故而我们可以进一步优化以上用来串接两个字符串的代码// sprintf(a_buffer, %s%s, a_string, another_string); char *p stpcpy(a_buffer, a_string); strcpy(p, another_string);防止对内存分配进行过度无根据使用这种对内存分配的过度无根据使用是致使 C 程序性能处于低下状态的常见缘由这个缘由还经常呈现为两种完全相反的趋向拿那种平常会用到的把给定的路径跟文件名串接起来并且去读取文件内容的函数当作例子函数的原型是下面这样char *get_file_contents_under_dir(const char *path, const char *fname);在此之中参数path被用来指定路径fname被用于指定文件名。这个函数要把path和fname串接成一个完整的路径名接着依据文件长度分配一个缓冲区再将文件的内容读取到该缓冲区最终返回缓冲区的地址。只看在那个函数里头专门用来把path和fname连接起来从而生成完整路径名的代码呢有一种很简易的实现方式那就是去调用asprintf()函数。#define _GNU_SOURCE #include char *get_file_contents_under_dir(const char *path, const char *fname) { char *full_path; if (asprintf(full_path, %s/%s, path, fname) 0) { assert(full_path); } else goto failed; char *buff NULL; /* 略去打开文件并读取其内容的代码。 */ ... free(full_path); return buff; failed: return NULL; }仅因要对两个字符串进行串接便去调用asprintf()函数明显是“杀鸡用牛刀”。此外asprintf()并非标准接口乃是GNU扩展接口存在一定的平台兼容性方面的问题。为此我们可以做一些调整#include char *get_file_contents_under_dir(const char *path, const char *fname) { char *full_path; full_path malloc(strlen(path) strlen(fname) 2); if (full_path NULL) { goto failed; } strcpy(full_path, path); strcat(full_path, /); strcat(full_path, fname); char *buff; ... free(full_path); return buff; failed: return NULL; }上述那种调整避免了出现“杀鸡用牛刀”这种不够合适的状况。不过要考虑到在绝大多数的情形之下一个文件的完整路径名其长度也就仅仅只有几百字节而已。那这种情况下去定义一个长度足够长的局部变量把它当作缓冲区来使用就好了根本完全没有必要去调用像 malloc() 这类函数到堆里面去动态地分配与之对应的缓冲区。除此之外C99 是支持变长数组也就是 Variable Length Array简称为 VLA 的。所以我们能够借助变长数组来定义这个缓冲区。进一步调整后的实现如下char *get_file_contents_under_dir(const char *path, const char *fname) { char full_path[strlen(path) strlen(fname) 2]; strcpy(full_path, path); strcat(full_path, /); strcat(full_path, fname); char *contents; ... free(full_path); return contents; failed: return NULL; }然而对于程序里运用变长数组的方式当出现path以及fname这两个字符串的长度加起来大于一个内存页也就是通常所说的4 KB的这么一种情况时系统就得去分配一整个完整的物理内存页以此来容纳那个缓冲区如此一来便会在一定程度让程序执行之中的效率有所下降。另外仍还有少数的编译器并不支持变长数组或者其在内部的实现并非是从栈当中去分配空间而是依旧从堆里分配空间只不过是在函数返回之前自动完成了缓冲区的释放罢了。所以针对此类缓冲区的分配操作在实际情形里更为有效的方式是依据即将分配的那种缓冲区的大小灵活地运用栈空间或者自行去分配合适的栈空间大小。char *get_file_contents_under_dir(const char *path, const char *fname) { /* 从栈中分配一个覆盖绝大多数路径长度的缓冲区。*/ char stack_buff[1024]; char *full_path; size_t full_path_len strlen(path) strlen(fname) 2; if (full_path_len sizeof(stack_buff)) { /* 如果用于完整路径长度的缓冲区之大小超过预定义的栈缓冲区 则执行动态分配。 */ if ((full_path malloc(full_path_len)) NULL) goto failed; } else full_path stack_buff; strcpy(full_path, path); strcat(full_path, /); strcat(full_path, fname); char *contents; ... /* 如果实际使用的缓冲区不是预定义的栈缓冲区则释放该缓冲区。 */ if (full_path ! stack_buff) free(full_path); return contents; failed: return NULL; }在程序里我们并非用 PATH_MAX 宏去界定栈缓冲区的大小。这是由于PATH_MAX 宏的值一般被设定为 4096而 4096 刚好就是一个物理内存页规模。运用 PATH_MAX 宏会造成额外物理内存页的分派甚至为了能容下最后的用于终止字符串的空字符我们一般会像这样去定义栈缓冲区char stack_buff[PATH_MAX 1];由于缓冲区超出了 4096 字节stack_buff 得用两个物理内存页来装这明显不划算。在绝大多数状况下要处理的完整路径名长度不会大于 1024 字节所以我们仅设定了 1024 字节大小的栈缓冲区。

更多文章