在没有分析器的情况下在C ++中测试代码速度的最佳方法,还是尝试没有意义?

时间:2010-06-27 17:01:23

标签: c++ profiling performance timer

在SO上,有很多关于性能分析的问题,但我似乎没有找到整体情况。涉及的问题很多,而且大多数Q&一次忽略除了少数几个,或者不为其提议辩护。

我想知道什么。如果我有两个功能做同样的事情,我很好奇速度的差异,没有外部工具,定时器测试这个是否有意义,或者在测试中编译会影响结果吗?

我问这个是因为如果它是明智的,作为一个C ++程序员,我想知道它应该如何做到最好,因为它们比使用外部工具简单得多。如果它有意义,让我们继续讨论所有可能的陷阱:

考虑这个例子。以下代码显示了执行相同操作的两种方法:

#include <algorithm>
#include <ctime>
#include <iostream>

typedef unsigned char byte;

inline
void
swapBytes( void* in, size_t n )
{
   for( size_t lo=0, hi=n-1; hi>lo; ++lo, --hi )

      in[lo] ^= in[hi]
   ,  in[hi] ^= in[lo]
   ,  in[lo] ^= in[hi] ;
}

int
main()
{
         byte    arr[9]     = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' };
   const int     iterations = 100000000;
         clock_t begin      = clock();

   for( int i=iterations; i!=0; --i ) 

      swapBytes( arr, 8 );

   clock_t middle = clock();

   for( int i=iterations; i!=0; --i ) 

      std::reverse( arr, arr+8 );

   clock_t end = clock();

   double secSwap = (double) ( middle-begin ) / CLOCKS_PER_SEC;
   double secReve = (double) ( end-middle   ) / CLOCKS_PER_SEC;


   std::cout << "swapBytes,    for:    "   << iterations << " times takes: " << middle-begin
             << " clock ticks, which is: " << secSwap    << "sec."           << std::endl;

   std::cout << "std::reverse, for:    "   << iterations << " times takes: " << end-middle
             << " clock ticks, which is: " << secReve    << "sec."           << std::endl;

   std::cin.get();
   return 0;
}

// Output:

// Release:
//  swapBytes,    for: 100000000 times takes: 3000 clock ticks, which is: 3sec.
//  std::reverse, for: 100000000 times takes: 1437 clock ticks, which is: 1.437sec.

// Debug:
//  swapBytes,    for: 10000000 times takes: 1781  clock ticks, which is: 1.781sec.
//  std::reverse, for: 10000000 times takes: 12781 clock ticks, which is: 12.781sec.

问题:

  1. 使用哪些计时器以及如何获得所讨论代码实际消耗的CPU时间?
  2. 编译器优化有什么影响(因为这些函数只是来回交换字节,最有效的事情显然是什么都不做)?
  3. 考虑到此处显示的结果,您认为它们是否准确(我可以向您保证多次运行会产生非常相似的结果)?如果是的话,考虑到自定义函数的简单性,你能解释一下std :: reverse如何变得如此之快。我没有用于此测试的vc ++版本的源代码,但是来自GNU的here is the implementation。归结为函数iter_swap,这对我来说是完全不可理解的。预计这也会比自定义函数快两倍,如果是,为什么?
  4. 一些设想:

    1. 似乎提出了两个高精度计时器:clock()QueryPerformanceCounter(在Windows上)。显然我们想测量代码的cpu时间而不是实时,但据我所知,这些函数没有提供该功能,因此系统上的其他进程会干扰测量。 gnu c库上的This page似乎与此相矛盾,但是当我在vc ++中设置断点时,调试过程会获得大量时钟滴答,即使它被挂起(我没有在gnu下测试过)。我是否遗漏了替代计数器,或者我们是否至少需要特殊的库或类?如果没有,在这个例子中时钟是否足够好或者是否有理由使用QueryPerformanceCounter?

    2. 如果没有调试,反汇编和分析工具,我们可以确定什么?有什么事实发生吗?函数调用是否内联?在检查调试器时,字节实际上是交换的,但我宁愿从理论上知道为什么,而不是从测试。

    3. 感谢您的任何指示。

      更新

      感谢来自hinttojas,swapBytes函数现在运行速度与std :: reverse一样快。我没有意识到在一个字节的情况下临时副本必须只是一个寄存器,因此非常快。优雅可以使你失明。

      inline
      void
      swapBytes( byte* in, size_t n )
      {
         byte t;
      
         for( int i=0; i<7-i; ++i )
          {
              t       = in[i];
              in[i]   = in[7-i];
              in[7-i] = t;
          }
      }
      

      感谢来自tipChrisW我发现在Windows上,您可以通过Windows Management Instrumentation获取(读取:您的)流程消耗的实际CPU时间。这绝对看起来比高精度计数器更有趣。

8 个答案:

答案 0 :(得分:4)

  

显然我们想测量代码的cpu时间而不是实时,但据我所知,这些函数没有提供该功能,因此系统上的其他进程会干扰测量。

我做了两件事,以确保挂钟时间和CPU时间大致相同:

  • 测试相当长的时间,即几秒钟(例如通过测试数千次迭代的循环)

  • 测试机器是否或多或少相对闲置,除了我正在测试的任何东西。

或者,如果您只想测量/更准确地测量每个线程的CPU时间,那么它可用作性能计数器(参见例如perfmon.exe)。

  

如果没有调试,反汇编和分析工具,我们可以确定什么?

几乎没有(除了I / O往往相对较慢)。

答案 1 :(得分:2)

为了回答你的主要问题,它“反向”算法只是从数组中交换元素而不是对数组元素进行操作。

答案 2 :(得分:2)

说你问两个问题是否安全?

  • 哪一个更快,多少?

  • 为什么它更快?

首先,您不需要高精度计时器。您需要做的就是“足够长时间”运行它们并使用低精度计时器进行测量。 (我很老式,我的手表具有秒表功能,而且完全足够好。)

对于第二个,当然可以在调试器下运行代码并在指令级单步执行。由于基本操作非常简单,因此您可以轻松查看基本周期所需的指令大致。

想想简单。表演不是一个难题。通常,人们会尝试发现问题this is a simple approach

答案 3 :(得分:2)

如果您需要高分辨率计时,请在Windows上使用QueryPerformanceCounter。计数器精度取决于CPU,但它可以达到每个时钟脉冲。但是,在现实世界中进行剖析总是一个更好的主意。

答案 4 :(得分:2)

(此答案特定于Windows XP和32位VC ++编译器。)

计算小位代码的最简单方法是CPU的时间戳计数器。这是一个64位值,是到目前为止运行的CPU周期数的计数,这大约是你要获得的精确分辨率。你得到的实际数字并不是特别有用,但如果你平均了几次各种竞争方法的运行,那么你就可以用这种方式进行比较。结果有点吵,但仍然有效用于比较目的。

要阅读时间戳计数器,请使用以下代码:

LARGE_INTEGER tsc;
__asm {
    cpuid
    rdtsc
    mov tsc.LowPart,eax
    mov tsc.HighPart,edx
}

cpuid指令用于确保没有任何不完整的指令等待完成。)

这种方法有四点值得注意。

首先,由于内联汇编语言,它在MS的x64编译器上无法正常工作。 (你必须创建一个带有函数的.ASM文件。读者练习;我不知道细节。)

其次,为了避免循环计数器在不同的核心/线程/你有什么不同步的问题,您可能会发现有必要设置进程的亲和性,以便它只在一个特定的执行单元上运行。 (然后......你可能不会。)

第三,您肯定要检查生成的汇编语言,以确保编译器大致生成您期望的代码。注意要删除的代码,内联函数,等等。

最后,结果相当嘈杂。循环计数器计算在所有事情上花费的周期,包括等待缓存,运行其他进程所花费的时间,在操作系统本身花费的时间等等。不幸的是,不可能(在Windows下,至少)只为您的进程计时。所以,我建议运行测试中的代码很多次(几万)并计算出平均值。这不是很狡猾,但无论如何它似乎都给我带来了有用的结果。

答案 5 :(得分:1)

你有什么东西可以对付个人资料吗?他们帮助了很多。既然您使用的是WinXP,那么您应该尝试一下vtune的试用版。尝试调用图采样测试并查看被调用函数的自身时间和总时间。没有更好的方法来调整你的程序,这样它就是最快的,而不是一个汇编天才(而且是一个真正特别的)。

有些人似乎对剖析师过敏。我曾经是其中之一,并且认为我最了解我的热点所在。对于明显的算法效率低下,我经常是正确的,但对于更多的微优化案例,实际上总是不正确的。只需在不改变任何逻辑的情况下重写函数(例如:重新排序,将特殊情况代码放在单独的非内联函数中等)可以使函数快十几倍,即使最好的反汇编专家通常也无法预测没有探查器。

至于仅仅依靠简单的时序测试,它们是非常有问题的。当前的测试并不是那么糟糕,但是编写时序测试是一个非常常见的错误,优化器将优化掉死代码并最终测试基本上完成nop甚至什么都不做的时间。你应该有一些知识来解释反汇编,以确保编译器没有这样做。

这样的时序测试也有可能显着偏向结果,因为很多只是涉及在同一个循环中反复运行代码,这往往只是测试代码在所有内存中的影响。缓存,所有分支预测都能完美地运行。它通常只是向您展示最佳案例场景,而不向您展示平均的真实案例。

根据现实世界的时间测试稍微好一些;更接近你的应用程序将在高层做的事情。它不会为您提供有关花费多少时间的具体信息,但这正是分析器的目的。

答案 6 :(得分:1)

我认为,任何有能力回答你所有问题的人都会非常忙于回答你的所有问题。在实践中,提出一个明确定义的问题可能更有效。通过这种方式,您可能希望得到明确的答案,您可以收集这些答案并开始走向智慧。

所以,无论如何,也许我可以回答你关于在Windows上使用哪个时钟的问题。

clock()不被视为高精度时钟。如果查看CLOCKS_PER_SEC的值,您会看到它的分辨率为1毫秒。只有在计时非常长的例程或具有10000次迭代的循环时,这才适用。正如您所指出的,如果您尝试重复一个简单的方法10000次,以便获得可以使用clock()测量的时间,编译器可能会介入并优化整个事情。

所以,实际上,唯一使用的时钟是QueryPerformanceCounter()

答案 7 :(得分:-3)

什?如何在没有分析器的情况下测量速度? 测量速度的行为是分析!问题相当于“我怎样才能编写自己的探查器?”答案显然是“不要”。

此外,你应该首先使用std::swap,这样就完全无效了。

-1表示没有意义。