【问题标题】:*str and *str++*str 和 *str++
【发布时间】:2011-09-05 00:53:14
【问题描述】:

我有这个代码(我的 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 秒!

为什么差别这么大?

【问题讨论】:

  • 为什么使用双精度而不是无符号长整数?此外,您应该尝试在没有优化的情况下进行编译并查看结果。哦,你应该同时运行大约 20 次并计算平均持续时间。
  • 分支预测失败?不必要的数据副本?尝试查看生成的程序集。另外,请尝试开启优化,它可能会解决问题。
  • 您使用哪种编译器?我用我的 gcc 4.4.5 运行它,它们几乎需要相同的时间,大约 2 秒。将 i 设置为 110021100,它们都使用大约 19 秒。
  • 所以这取决于我猜使用的是哪个编译器。 (我在 gcc 编译器中使用代码块)
  • if (*str) do { ... } while (*++str); 呢?

标签: c pointers char


【解决方案1】:

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

while (*str++) 的真正含义是:

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

相比之下,当您将 str++; 作为单个语句写入 while 循环体时,它处于 void 上下文中,因此不会获取旧值,因为它不需要。

总而言之,在*str++ 的情况下,您在循环的每次迭代中都有一个赋值、2 个增量和一个跳转。在另一种情况下,您只有 2 个增量和一个跳转。

【讨论】:

  • 但是在这两种情况下都有一个test(*str)inc(str) -- 从高层的角度来看,所做的工作是相同的。
  • 体面的编译器应该没关系。
  • @pst:原则上,后期增量总是涉及复制。在实践中,通常可以省略副本,但取决于编译器、语句的确切上下文以及优化设置,它可能会或可能不会实际完成。
  • @delnan: 好吧,但是在非空上下文中,例如在 while 语句的条件下,无法避免复制。
  • @Blagovest Buyukliev - 可以避免临时副本。它不需要复制数据,只需测试数据是否为零。然后它可以在不复制的情况下执行增量,然后使用测试结果执行必要的流控制。
【解决方案2】:

在 ideone.com 上尝试一下,我用 *str++ here 执行了大约 0.5 秒。没有,它只需要一秒钟多一点(here)。使用 *str++ 更快。也许对 *str++ 进行优化可以更有效地完成。

【讨论】:

    【解决方案3】:

    这取决于您的编译器、编译器标志和您的架构。使用 Apple 的 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++ 版本可以编译完全相同(因为对str 的更改在slen 之外不可见,当实际发生增量时并不重要),或者循环体可以是:

    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
    

    【讨论】:

      【解决方案4】:

      其他人已经提供了一些出色的评论,包括对生成的汇编代码的分析。我强烈建议您仔细阅读它们。正如他们所指出的那样,如果没有一些量化,就无法真正回答这类问题,所以让我们玩一下。

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

      除了您已经展示的两种算法之外,我将展示第三种选择,它根本不涉及使用计数器,而是依靠减法,我将通过投入来混合std::strlen,只是想看看会发生什么。这将是一个有趣的失败。

      通过电视的魔力,我们的小程序已经编写好了,所以我们用gcc -std=c++11 -O3 speed.c 编译它,然后开始生成一些数据。我已经做了两个单独的图表,一个用于大小从 32 到 8192 字节的字符串,另一个用于大小从 16384 一直到 1048576 字节长的字符串。在下图中,Y 轴是以 纳秒 为单位消耗的时间,X 轴以字节为单位显示字符串的长度。

      事不宜迟,让我们看看从 32 到 8192 字节的“小”字符串的性能:

      现在这个很有趣。 std::strlen 函数不仅在所有方面都表现出色,而且由于它的性能更加稳定,所以它也很有热情。

      如果我们查看更大的字符串,从 16384 一直到 1048576 字节长,情况会改变吗?

      有点。差异变得更加明显。作为我们自定义编写的函数,std::strlen 继续表现出色。

      一个有趣的观察是,您不一定将 C++ 指令的数量(甚至汇编指令的数量)转换为性能,因为由较少指令组成的函数有时需要更长的执行时间。

      一个更有趣且重要的观察是注意str::strlen 函数的执行情况。

      那么这一切给我们带来了什么?

      第一个结论:不要重新发明轮子。使用您可用的标准功能。它们不仅已经编写好了,而且还进行了非常严格的优化,并且几乎肯定会胜过您可以编写的任何东西,除非您是 Agner Fog

      第二个结论:除非您有来自分析器的硬数据表明特定代码段或函数在您的应用程序中是热点,否则不要费心优化代码。众所周知,程序员不擅长通过查看高级函数来检测热点。

      第三个结论:更喜欢算法优化以提高代码的性能。下定决心,让编译器随机播放位。

      您最初的问题是:“为什么函数 slen2slen1 慢?”我可以说,如果没有更多信息,回答起来并不容易,即便如此,它也可能比你关心的时间更长、参与度更高。相反,我要说的是:

      谁在乎为什么?你为什么还要为此烦恼?使用std::strlen - 这比你可以安装的任何东西都要好 - 并继续解决更重要的问题 - 因为我确信这不是你的应用程序中的最大问题。

      【讨论】:

        猜你喜欢
        • 2015-10-06
        • 2015-07-17
        • 1970-01-01
        • 1970-01-01
        • 2020-08-25
        • 2022-01-20
        • 1970-01-01
        • 2021-04-24
        • 1970-01-01
        相关资源
        最近更新 更多