空字符串的strcmp函数

31

我正在审查一些代码,看到有人进行了以下操作:

if (0 == strcmp(foo,""))

我很好奇,因为我认为这样做会更快:

if (foo[0] == '\0')

这样做是否正确,或者说strcmp已经被优化到让它们变得相同了。(我意识到即使存在一些差异,它们也很小,但我想通过使用我的方法至少可以节省几条指令。)


你对这个模式提出质疑是正确的,我喜欢你的方法更多。 - SingleNegationElimination
12
如果该字符串长度为1兆,则必须在找到终止空字符之前检查100万个字节;当您只关心字符串具有零个或多个字符的情况时,这将会带来相当大的损失。 - SingleNegationElimination
3
你的方法无疑会更快。即使你忽略了strcmp函数的调用开销,它最终也必须进行相同的内存访问。因此,它只能以相同或更慢的速度运行。 - Mike Kwan
2
@Mike:我认为问题的实质更多地涉及strcmp可能比直接数组访问做出更明智的操作,例如缓冲区溢出等情况。我认为答案是,在这种情况下,没有任何情况需要使用strcmp。 - SingleNegationElimination
@x4u提供了一个非常好的选项。我检查了这些条件,并发现“if(!str)”比“if(0 == strcmp(foo,“”))”少一条指令。对于x64架构的Microsoft编译器,分别产生3个instr(12字节)和4个instr(18字节)。顺便说一句,编译器对“if(!str)”和“if(str [0] =='\ 0')”生成相同的代码。 - user3124812
显示剩余2条评论
8个回答

11

9

strcmp() 是一个函数调用,因此具有函数调用开销。foo[0] 是对数组的直接访问,因此显然更快。


5

在这种情况下,我认为使用strcmp没有任何优势。编译器可能足够聪明以将其优化掉,但它不会比直接检查'\0'字节更快。实现者可能选择这个结构是因为他认为它更易读,但我认为在这种情况下这是一种口味问题。虽然我会写一个稍微不同的检查,因为这似乎是用于检查空字符串的最常用习语:

if( !*str )

并且。
if( *str )

检查非空字符串。


是的,我更喜欢这样,但在我的工作中,我们有一些严格的指南,要求在条件语句中明确表达。所以即使它没有太多意义,我也必须明确检查 '\0' 或 0。 - Pablitorun

4
+1给Gui13提供了一个链接,链接指向gcc stdlib strcmp的源代码(http://sourceware.org/git/?p=glibc.git;a=blob;f=string/strcmp.c;h=bd53c05c6e21130b091bd75c3fc93872dd71fe4b;hb=HEAD)!
你说的没错,strcmp永远比直接比较慢[1],但问题是,编译器会优化它吗?我一开始很害怕尝试测量这个,但我惊喜地发现它非常容易。我的示例代码是(省略头文件):
bool isEmpty(char * str) {
   return 0==std::strcmp(str,"");
}
bool isEmpty2(char * str) {
   return str[0]==0;
}

我试着用gcc -S -o- emptystrcmptest.cc编译,然后用gcc -S -O2 -o- emptystrcmptest.cc编译。令人惊喜的是,尽管我不太能读懂汇编语言,但未优化版本明显显示出差异,优化版本则清楚地显示出两个函数生成相同的汇编代码。因此,总体而言,不必担心这种级别的优化。

如果您在嵌入式系统中使用编译器并且知道它不能处理这种简单的优化(或者根本没有标准库),请使用手动编写的特殊情况版本。

如果您正在正常编码,请使用更易读的版本(在上下文中可能是strcmp、strlen或[0]==0)。

如果您正在编写预计每秒调用数以千计或百万计的高效代码,则应(a)测试哪个更有效并且(b)如果可读性版本实际上太慢,则尝试编写可以编译为更好的汇编代码的代码。

使用gcc -S -o- emptystrcmptest.cc

            .file   "emptystrcmptest.cc"
            .section .rdata,"dr"
    LC0:
            .ascii "\0"
            .text
            .align 2
    .globl __Z7isEmptyPc
            .def    __Z7isEmptyPc;  .scl    2;      .type   32;     .endef
    __Z7isEmptyPc:
            pushl   %ebp
            movl    %esp, %ebp
            subl    $24, %esp
            movl    $LC0, 4(%esp)
            movl    8(%ebp), %eax
            movl    %eax, (%esp)
            call    _strcmp
            movl    %eax, -4(%ebp)
            cmpl    $0, -4(%ebp)
            sete    %al
            movzbl  %al, %eax
            movl    %eax, -4(%ebp)
            movl    -4(%ebp), %eax
            leave
            ret
            .align 2
    .globl __Z8isEmpty2Pc
            .def    __Z8isEmpty2Pc; .scl    2;      .type   32;     .endef
    __Z8isEmpty2Pc:
            pushl   %ebp
            movl    %esp, %ebp
            movl    8(%ebp), %eax
            cmpb    $0, (%eax)
            sete    %al
            movzbl  %al, %eax
            popl    %ebp
            ret
    emptystrcmptest.cc:10:2: warning: no newline at end of file
            .def    _strcmp;        .scl    2;      .type   32;     .endef

使用gcc -S -O2 -o- emptystrcmptest.cc命令:

        .file   "emptystrcmptest.cc"
emptystrcmptest.cc:10:2: warning: no newline at end of file
        .text
        .align 2
        .p2align 4,,15
.globl __Z7isEmptyPc
        .def    __Z7isEmptyPc;  .scl    2;      .type   32;     .endef
__Z7isEmptyPc:
        pushl   %ebp
        movl    %esp, %ebp
        movl    8(%ebp), %eax
        popl    %ebp
        cmpb    $0, (%eax)
        sete    %al
        movzbl  %al, %eax
        ret
        .align 2
        .p2align 4,,15
.globl __Z8isEmpty2Pc
        .def    __Z8isEmpty2Pc; .scl    2;      .type   32;     .endef
__Z8isEmpty2Pc:
        pushl   %ebp
        movl    %esp, %ebp
        movl    8(%ebp), %eax
        popl    %ebp
        cmpb    $0, (%eax)
        sete    %al
        movzbl  %al, %eax
        ret

[1] 尽管如此,需要注意的是 -- 在任何比直接测试零更复杂的情况下,库和编译器代码通常会比手工编写的代码更好。


+1 @Jack。这很好地证明了@Brandon Moretz的评论。好帖子。就纯可读性而言,我仍然不确定我更喜欢哪个,但这绝对在过早优化方面提出了一个很好的观点。 - pickypg

1
一个好的优化编译器可能会优化掉函数调用,然后从内联函数中消除循环。虽然你的方法不可能更慢,但有可能速度相同。

0

显然,这样会更快,如果您打算继续使用它,将自己的代码放在内联函数中甚至宏中可能是值得的:

int isEmpty(const char *string)
{
    return ! *string;
}

int isNotEmpty(const char *string)
{
    return *string;
}

int isNullOrEmpty(const char *string)
{
    return string == NULL || ! *string;
}

int isNotNullOrEmpty(const char *string)
{
    return string != NULL && *string;
}

并让编译器为您进行优化。无论如何,strcmp 最终需要检查 '\0',因此您始终至少与它相等。(老实说,我可能会让编译器优化上述内容的内部共享,例如,isEmpty 可能只是翻转 isNotEmpty


我非常确定,确定 f() == !a() 远远超出了任何优化器的能力。 - mikerobi
@mike 正确,但它仍然可以优化调用,使其成为内联调用,而不是将相同的指针推入堆栈。这也可以防止潜在的“非标准”代码弄脏代码库。 - pickypg

0

访问数组的执行时间为1,比函数更快。


0
这是最微观的优化,但我想如果在索引foo之前添加一个空值检查(除非你知道它永远不会为空),那么它在技术上将节省函数调用的开销。

1
我认为你误解了重点。OP并不是在检查空指针,而是检查零长度字符串。既然效果相同,所以执行直接检查和strcmp调用没有任何意义。 - R.. GitHub STOP HELPING ICE
1
@R 我认为你应该重新阅读我的回复。我只是在说他应该在盲目索引指针之前对其执行空指针检查。我没有提到调用strcmp的事情。 - Brandon Moretz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接