2011-05-30 80 views
8

我有这样的代码(我的strlen函数)* STR和*海峡++

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

为什么使用double而不是unsigned long?另外,您应该尝试编译而不进行优化并查看结果。哦,你应该运行约二十次,并计算平均持续时间。 – 2011-05-30 20:01:09

+0

分支预测失败?不必要的数据副本?尝试查看生成的程序集。此外,尝试开启优化,它可能会解决问题。 – dmckee 2011-05-30 20:02:25

+2

你使用什么样的编译器?我用gcc 4.4.5运行它,它们几乎在同一时间,大约2s。随着我设置为110021100,他们都使用大约19秒。 – 2011-05-30 20:03:10

回答

6

可能是因为后增量操作符(在while语句的条件中使用)涉及使用其旧值保留该变量的临时副本。

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

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

与此相反,当你写str++;作为while循环的身体一个语句,它是在一个无效的情况下,因此,旧的值不因为不需要而被抓取。

总之,在你有一个分配,增量为2,并在每个循环迭代一个跳*str++情况。在另一种情况下,你只有2个增量和一个跳跃。

+0

但是在两种情况下都有一个'test(* str)'和'inc(str)' - 从高级角度来看,所做的工作是一样的。 – 2011-05-30 20:06:51

+4

它与正派的编译器无关。 – delnan 2011-05-30 20:12:02

+2

@pst:原则上,增量后总是包含一个副本。在实践中,复制通常可能会被忽略,但取决于编译器,声明的确切上下文以及优化设置,它可能实际上可能会或可能不会实现。 – dmckee 2011-05-30 20:12:26

2

上ideone.com尝试了这一点,我与*海峡++ here约0.5秒执行。没有,它需要超过一秒钟(here)。使用* 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++版本可以被编译完全相同(因为更改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 
1

其他人已经提供了一些优秀的commen包括对生成的汇编代码的分析。我强烈建议你仔细阅读。正如他们指出的,这种问题如果没有量化就不能真正回答,所以让我们一起玩吧。

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

除了你已经显示的两种算法之外,我将展示第三个选项,它根本不涉及使用计数器,而是依赖于减法,我将通过投掷来混合东西在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 - 这比任何你可以设置的更好 - 然后继续解决更重要的问题 - 因为我确信这个不是是你应用程序中最大的问题。

相关问题