你的解决方案只要参数是有效的以空字符结尾的字符串就可以正常工作,这是最重要的,从这个角度来看,你做得很明智。其他更复杂的解决方案并不符合这个目标。编译器会内联strlen(".foo"),并且应该能够确定两个实例的strlen(str)返回相同的值,因此生成单个调用(
(clang和gcc))。然而,在我看来,更加优雅的方式是计算长度一次并使用memcmp()而不是需要更多工作并且不能内联的strcmp()。你还应该将str定义为const char *以实现const正确性,并在使用常量字符串或字符串文字调用函数时防止警告。
测试特定的".foo"
后缀是更一般问题的特例:测试一个字符串是否为另一个字符串的后缀。
以下是一个简单且高效的解决方案:
#include <string.h>
int strEndsWith(const char *s, const char *suff) {
size_t slen = strlen(s);
size_t sufflen = strlen(suff);
return slen >= sufflen && !memcmp(s + slen - sufflen, suff, sufflen);
}
int strEndsWithFoo(const char *s) {
return strEndsWith(s, ".foo");
}
这段代码非常简单且通用,现代编译器可以高效地内联strEndsWithFoo
。正如可以在GodBolt的编译器资源管理器上验证的那样,clang 12.0.0会在编译时计算".foo"
的长度,并将memcmp()
作为单个cmp
指令内联,生成仅有12个x86_64指令:
strEndsWithFoo: # @strEndsWithFoo
pushq %rbx
movq %rdi, %rbx
callq strlen
movq %rax, %rcx
xorl %eax, %eax
cmpq $4, %rcx
jb .LBB1_2
xorl %eax, %eax
cmpl $1869571630, -4(%rbx,%rcx) # imm = 0x6F6F662E
sete %al
.LBB1_2:
popq %rbx
retq
gcc 11.2 生成非常相似的代码,也是12条指令:
strEndsWithFoo:
pushq %rbx
movq %rdi, %rbx
call strlen
xorl %r8d, %r8d
cmpq $3, %rax
jbe .L7
xorl %r8d, %r8d
cmpl $1869571630, -4(%rbx,%rax)
sete %r8b
.L7:
movl %r8d, %eax
popq %rbx
ret
Intel的ICC编译器生成一组冗长而复杂的SIMD指令,即使在英特尔处理器上也可能难以跟踪并且效率可能更低。性能严重依赖于
strlen()
库函数的效率,因此基准测试应包括各种字符串长度的分布。
关于“最有效的解决方案是什么”的绝对答案并不存在,但简单并不排除高效性,简单直接的代码更易于验证。当它结合了简单性,正确性和效率时,就达到了“优雅”。
引用
Brian Kernighan的话:
- 控制复杂性是计算机编程的本质。
软件工具(1976年),第319页(与P.J.Plauger合著)。
- 每个人都知道调试比首次编写程序难两倍。因此,如果您在编写程序时越聪明,那么您将如何调试它?
“编程风格的要素”,第2版,第2章。