sizeof vs strlen - 关于代码可读性、性能考量和编译器优化

1、起因

经常在咱们代码里面见到sizeof(“HEADER”)这类代码来计算常量字符串的长度,例如上次的一个代码review:

之所以这么写可能基于以下几点考虑:

(1)sizeof()是运算符而不是函数调用,编译时确定而不是运行时执行,因此不占用运行时时间

(2)strlen()是GLIBC标准库函数,运行时需要进行函数调用,因此耗时

2、分析

但是实际情况确实如咱们预想一致吗?

首先做一个性能测试,分别调用sizeof/strlen() 1000W次的对比结果,程序如下:

结果: sizeof_O0.time

sizeof_O2.time

strlen_O0.time

strlen_O2.time

从以上对比结果上可以看出两个方式对CPU的耗时基本是一致的。

很奇怪,然后继续研究一下反汇编后的代码:

汇编代码:sizeof with O0

汇编代码:strlen with O0

很明显,即使在O0情况下,编译器默认也对strlen进行了优化,这也是两者结果是基本一样的原因。

另外常量字符串长度加大并不影响该优化。

3、进一步分析

查看strlen调用的汇编可以看出,对字符串进行长度计算的时候,并没有调用bl <strlen>, 而是直接返回其长度5。

常规理解上来看,编译器在编译代码时,看到strlen,因为是标准库里的函数,会进行标准函数调用,并且去链接gcc的标准库。但是在这个例程里,编译器直接给出了字符串的长度。

本来是怀疑编译器优化级别的原因,但是即使把O2改为O0,汇编上看结果还是一样的。

3.1 原因:编译器优化

(1)首先,为什么编译后的汇编代码里会直接给出了常量字符串的长度?

GCC有一个内置的strlen函数,但在某些情况下,编译器可能会执行一种称为常量折叠的优化,尤其是在它能够确定字符串的长度在编译时是已知的情况下。

(2)为什么没有函数调用

在C语言中,有些函数是作为编译器内置的,这意味着编译器会直接将它们替换为相应的机器代码,而不是生成函数调用的代码。这通常用于性能优化,因为内置函数的调用开销更小,而且编译器可以针对具体的调用情况进行优化。

3.2 常量折叠

在编译期间,C语言的常量会被替换为其具体的值或表达式。这个过程被称为常量折叠或常量表达式计算。编译器会在编译阶段直接将常量的值替换到代码中,从而减少程序运行时的计算量和内存消耗。

常量折叠是指在编译时识别和评估常量表达式的过程,而不是在运行时计算它们。常量表达式中的术语通常是简单的字面意义,例如整数字面意义2,但它们也可能是变量,其值在编译时已经知道。考虑一下这个语句。

比如: i = 20* 20 * 10;

上面这个表达式,有2个乘号,很少有编译器的指令会对2个乘号做运算,并存到一个寄存器。所以在编译的时候i会被直接替换计算后的值4000

常量折叠可以利用算术特性。如果x是数字,即使编译器不知道x的值,0*x的值也是零。

常数折叠可能不仅仅适用于数字。字符串和常量字符串的拼接可以被常量折叠。像 "abc"+"def"这样的代码可以被替换为 "abcdef"。

总的来说,C语言的常量在编译期间的处理方式取决于常量的类型和定义方式,编译器会尽可能地优化常量的处理,以提高程序的性能和效率。

3.3 内置函数替换

(1) 编译器是怎么知道哪些函数需要替换?

编译器知道内置函数的实现并将其替换为机器代码,是因为这些函数的实现是编译器的一部分,而不是从外部库中链接进来的。这些内置函数通常包括C语言标准库中的函数(如printf、scanf、strlen等),以及编译器提供的其他常用函数(如数学函数、字符串处理函数等)。

当编译器解析源代码时,它会识别出这些内置函数的调用。编译器内部有一个预定义的内置函数列表,这些函数的实现通常位于编译器的libgcc或libstdc++库中。当编译器遇到一个内置函数调用时,它会将这个调用替换为对应的机器代码,而不是生成一个函数调用。

例如,当编译器遇到以下C代码时:

printf("Hello, World!");

它会将printf调用替换为libgcc中printf函数的机器代码,而不是生成一个函数调用。这使得调用更加快速,因为不需要跳转到外部函数的地址,也不需要执行额外的指令来执行函数调用。

如果不想编译器对内置函数进行替换,则可以使用-fno-builtin选项。-fno-builtin是GCC编译器的一个选项,用于控制编译器对内置函数的处理方式。当使用-fno-builtin选项时,是在告诉GCC不要将这些内置函数当作内置函数处理,而是按照普通的函数调用方式来编译它们。这意味着编译器会生成函数调用的代码,而不是直接替换为机器代码。

(2) 内置函数替换的时机

内置函数的替换通常发生在编译器的前端,也就是在编译器的词法分析器(lexer)、语法分析器(parser)和语义分析器(semantic analyzer)阶段。在这些阶段,编译器解析源代码,识别出函数调用,并决定是否将其替换为内置函数的实现。

内置函数的替换默认是编译器优化的一部分,但是也受到编译器配置和目标平台的影响。

(3) -O0可以取消内置函数的替换吗?

不,-O0选项不能取消内置函数的替换。-O0是GCC的优化级别选项,它表示不进行任何优化。然而,内置函数的替换是编译器默认的行为,即使在-O0优化级别下,内置函数仍然会被替换为机器代码。内置函数的替换是编译器实现的一部分,旨在提供高性能的调用,而不需要额外的函数调用开销。这种优化是编译器自动进行的,不受优化级别的影响。

(4) 关于strlen()函数的进一步分析

即使对于正常的动态字符串的函数调用,GNU提供的C标准库中strlen的实现实际上已经针对性能做了一些优化。比如每次测试四个字节来代替传统实现中每次测试一个字节的方法,以及通过跟预设值的位与操作来进行快速比较。具体可以参考glibc中的实现。

此外在一些平台上,strlen也会使用SIMD等指令集或并行处理来加速遍历过程。

另外即使在性能关键路径,比如在循环处理过程中,编译器也会根据代码上下文判断出strlen()的参数字符串在该自治代码块中是否恒定,来决定是否对其进行编码优化,如果满足条件,编译器会毫不犹豫的优化代码。

4、结论

  • 对于常量字符串来说,使用strlen并不会比sizeof降低性能,因为编译器对常量字符串进行了编译优化,且对内置函数进行了替换。

  • 代码可读性是我们写代码时要考虑的重要因素,因此在计算常量字符串长度时,strlen要比sizeof更好。

  • 性能对于非关键节点代码来说,并不是首要考虑因素。

  • 编译器默认已经做了很多优化。

注:

本例不考虑sizeof在C99中对动态数组的支持

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值