*str and *str++

8
我有这段代码(我的strlen函数)
size_t slen(const char *str)
{
    size_t len = 0;
    while (*str)
    {
        len++;
        str++;
    }
    return len;
}

在以下代码中,使用 while (*str++) 的程序执行时间要长得多:

while (*str++)
{
    len++;
}

我这样做是为了探测代码

int main()
{
    double i = 11002110;
    const char str[] = "long string here blablablablablablablabla"
    while (i--)
        slen(str);

    return 0;
}

在第一种情况下,执行时间约为6.7秒,而在第二种情况下(使用*str++),时间约为10秒!
为什么会有这么大的差异?

5
为什么使用双精度浮点数而不是无符号长整型?此外,你应该尝试在没有优化的情况下进行编译,并查看结果。哦,还有,你应该运行它们各自大约二十次并计算平均持续时间。 - user142019
分支预测失败?不必要的数据复制?试着看一下生成的汇编代码。另外,尝试开启优化,这可能会解决问题。 - dmckee --- ex-moderator kitten
2
你用的是什么编译器?我用我的gcc 4.4.5运行它,时间几乎相同,大约2秒。当i设置为110021100时,它们都使用约19秒。 - zw324
所以我猜这取决于你使用的编译器。(我正在使用带有gcc编译器的codeblocks) - Ulrira
你是否给编译器提供优化选项?(例如,在gcc中的“-O3”)如果没有,编译器可能会生成较慢的代码以便于调试。 - ikh
显示剩余2条评论
4个回答

6

很可能是因为后增运算符(用于while语句的条件)涉及将变量的旧值与临时副本一起保留。

while (*str++) 的真正意思是:

while (tmp = *str, ++str, tmp)
  ...

相比之下,当你在while循环的主体中写入str++;作为单个语句时,它处于void上下文中,因此旧值不会被获取,因为它不需要。
总结一下,在*str++的情况下,每次迭代循环都有一个赋值、两个递增和一个跳转。而在另一种情况下,你只有两个递增和一个跳转。

4
合格的编译器不应该有关紧要。 - user395760
2
@pst: 原则上,后增操作总是涉及到复制。实际上,副本通常可以被省略,但取决于编译器、语句的确切上下文和优化设置,可能会或可能不会真正执行该操作。 - dmckee --- ex-moderator kitten
@delnan:嗯,但在非void上下文中,例如在while语句的条件中,无法避免拷贝。 - Blagovest Buyukliev
@Blagovest Buyukliev - 可以非常避免使用临时副本。它不需要复制数据,只需测试数据是否为零。然后可以执行增量而不进行复制,然后使用测试结果执行必要的流程控制。 - Chris Lutz
这两个代码片段都涉及到后增量,然而我认为一个好的编译器会将后增量从条件中“解开”,并在机器码级别将第二个代码片段转换成类似第一个代码片段的形式。 - Lundin
显示剩余2条评论

2

在ideone.com上尝试,使用* str ++ 这里大约需要0.5秒执行时间。没有使用时,需要超过一秒钟(这里)。使用* str ++ 更快。也许通过优化可以更有效地使用* str ++。


1
这取决于您的编译器、编译器标志和架构。使用苹果的LLVM gcc 4.2.1,我在两个版本之间没有注意到性能上的明显变化,实际上也不应该有。一个好的编译器会将*str版本转换成类似以下的内容:

IA-32(AT&T语法):

slen:
        pushl %ebp             # Save old frame pointer
        movl  %esp, %ebp       # Initialize new frame pointer
        movl  -4(%ebp), %ecx   # Load str into %ecx
        xor   %eax, %eax       # Zero out %eax to hold len
loop:
        cmpb  (%ecx), $0       # Compare *str to 0
        je    done             # If *str is NUL, finish
        incl  %eax             # len++
        incl  %ecx             # str++
        j     loop             # Goto next iteration
done:
        popl  %ebp             # Restore old frame pointer
        ret                    # Return

*str++版本可以编译完全相同(因为slen外部看不到str的更改,实际发生增量的时间并不重要),或者循环体可以是:

loop:
        incl  %ecx             # str++
        cmpb  -1(%ecx), $0     # Compare *str to 0
        je    done             # If *str is NUL, finish
        incl  %eax             # len++
        j     loop             # Goto next iteration

1

其他人已经提供了一些很好的评论,包括对生成的汇编代码的分析。我强烈建议你仔细阅读它们。正如他们所指出的那样,这种问题实际上无法在没有一些量化的情况下回答,因此让我们来试试吧。

首先,我们需要一个程序。我们的计划是这样的:我们将生成长度为2的幂的字符串,并依次尝试所有函数。我们通过一次运行来预热缓存,然后单独使用我们可用的最高分辨率进行4096次迭代。完成后,我们将计算一些基本统计数据:最小值、最大值和简单移动平均值,并将其转储。然后我们可以进行一些基本分析。

除了你已经展示的两个算法之外,我还将展示第三个选项,它根本不涉及计数器,而是依靠减法,并且我会混合使用 std::strlen,只是为了看看会发生什么。这将是一场有趣的比拼。

通过电视的魔力,我们的小程序已经编写完成,所以我们使用“gcc -std=c++11 -O3 speed.c”进行编译并开始生成一些数据。我做了两个不同的图表,一个是字符串大小在32到8192字节之间的,另一个是字符串大小从16384一直到1048576字节长的。在以下图表中,Y轴表示消耗时间(以纳秒为单位),X轴显示字符串长度(以字节为单位)。
话不多说,让我们来看看32到8192字节的“小型”字符串的性能:

Performance Plot - Small Strings

现在这很有趣。不仅是std::strlen函数在各方面表现出色,而且它的性能稳定性也更高,因此它的表现更加快捷。
如果我们查看更长的字符串,从16384到1048576字节长,情况会发生变化吗?

enter image description here

有点类似。差异越来越明显。随着我们定制的函数变得越来越复杂,std::strlen 仍然表现出色。
一个有趣的观察是,你不能将 C++ 指令数量(甚至汇编指令数量)直接转化为性能,因为函数体包含更少指令的函数有时需要更长时间才能执行完毕。
更有趣、也更重要的一点是,要注意 str::strlen 函数的优异表现。
那么这些对我们意味着什么呢?
第一个结论:不要重复造轮子。使用已经提供给你的标准函数。它们不仅已经被编写好了,而且已经进行了非常非常大量的优化,除非你是Agner Fog,否则几乎肯定会胜过你自己编写的任何东西。
第二个结论:除非你有来自分析器的硬数据证明你的应用程序中特定代码段或函数是热点区域,否则不要费力优化代码。程序员很难通过查看高级函数来检测到热点区域。
第三个结论:优先采用算法优化以提高代码性能。让编译器处理位运算,用心去思考。
你最初的问题是:“为什么函数slen2比slen1慢?”我可以说,没有更多信息很难回答,即使有也可能比你想象中更长更复杂。相反,我会这样说:
谁在乎为什么?你为什么还要费心去解决这个问题?使用std::strlen——它比你自己编写的任何东西都要好——然后转向解决更重要的问题——因为我确信这不是你应用程序中最大的问题。

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