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

Posted

技术标签:

【中文标题】在没有分析器的情况下在 C++ 中测试代码速度的最佳方法,或者尝试没有意义?【英文标题】:Best way to test code speed in C++ without profiler, or does it not make sense to try? 【发布时间】:2010-06-27 17:01:23 【问题描述】:

关于 SO,有很多关于性能分析的问题,但我似乎没有找到全貌。涉及的问题很多,大多数问答一次都忽略了几个,或者不证明他们的建议是合理的。

我想知道什么。如果我有两个功能做同样的事情,并且我对速度上的差异感到好奇,那么在没有外部工具、使用计时器的情况下进行测试是否有意义,或者在测试中编译的这是否会对结果产生很大影响?

我问这个是因为如果它是明智的,作为一名 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.

问题:

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

沉思:

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

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

感谢任何指示。

更新

感谢来自tojas 的hint,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;
    

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

【问题讨论】:

您要问的是哪个操作系统?回到我写计时代码的时候,各种操作系统对正确的时钟有不同的 API 调用。 我正在 WindowsXP 上进行测试,但了解其他操作系统也同样有趣 在您第一次尝试使用探查器之后,值得尝试不使用探查器。 【参考方案1】:

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

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

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

在机器或多或少相对空闲时进行测试,除了我正在测试的任何东西。

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

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

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

【讨论】:

perfmon,是的,谢谢你提醒我。我知道它存在,而且非常方便,但你知道我们是否可以使用系统调用在我们的程序中获取这些信息? @ufotds - 很久以前,当我这样做时,我使用毛茸茸的调用来读取注册表的隐藏“性能”部分(调用很容易,但解析它们返回的二进制数据并不容易) )。现在,我不知道它可能会被“WMI”API 抽象出来。【参考方案2】:

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

【讨论】:

【参考方案3】:

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

【讨论】:

这也取决于它何时被调用。许多 CPU 会动态更改时钟频率。【参考方案4】:

可以肯定地说您要问两个问题吗?

哪个更快,速度快多少?

为什么它更快?

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

其次,您当然可以在调试器下运行代码并在指令级单步执行。由于基本操作如此简单,您将能够轻松地大致看出基本循环需要多少条指令。

想得简单。性能不是一个很难的主题。通常,人们试图发现问题,为此this is a simple approach。

【讨论】:

是的,甚至超过 2 个......但由于某种原因,可视化调试器不允许我进入 std::reverse,但我只尝试过发布模式。现在在调试中它可以工作,我实际上可以看到它与我在 swapBytes 更新中所写的内容完全相同,除了验证指针等......【参考方案5】:

(此答案特定于 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 下)只为您的进程计时。因此,我建议多次运行被测代码(数万次)并计算平均值。这不是很狡猾,但无论如何它似乎对我产生了有用的结果。

【讨论】:

嗨,谢谢你的 sn-p。我怀疑它是否具有此目的的实用价值,因为显然使用 WMI 可以仅测量您的进程,但我将它粘贴到一个简单的 c++ 程序中,它可以按原样工作。最重要的是,这是我第一次使用内联汇编,因为我的汇编知识相当惨淡……【参考方案6】:

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

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

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

所以,真的,唯一要使用的时钟是 QueryPerformanceCounter()

【讨论】:

【参考方案7】:

你有什么反对分析器的吗?他们帮了很多忙。既然你在WinXP上,你真的应该试一试vtune。尝试调用图抽样测试并查看被调用函数的自身时间和总时间。没有比这更好的方法来调整您的程序,使其在不成为装配天才(并且是真正杰出的天才)的情况下尽可能快。

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

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

此外,像这样的计时测试往往会显着影响结果,因为其中很多只是涉及在同一个循环中一遍又一遍地运行您的代码,这往往只是测试您的代码在所有内存都在缓存,所有分支预测都非常适合它。它通常只是向您展示最佳案例场景,而不向您展示实际的平均案例。

依赖于真实世界的时序测试会好一点;更接近于您的应用程序将在高级别上执行的操作。它不会详细说明什么需要花费多少时间,但这正是分析器的目的。

【讨论】:

我之前使用分析器来优化整个程序的性能,但考虑到对一些简单功能的好奇,调用几个定时器肯定比选择、下载、安装、阅读手册和开始使用更容易分析器。总而言之,理解像这样的底层内容和让你的软件以合理的性能运行是有区别的。对于后者,我很乐意使用分析器,并且 std::reverse 的速度很可能根本不会让我担心,除非我正在反转千兆字节...... 如果您只是追求可接受的性能而不是卓越的性能,那么可能需要进行时序测试。然而,重要的是要记住,虽然分析器可能需要一些时间来学习,但它只是你必须做一次的事情。在 vtune 中,只需使用调用图采样向导,选择您的 exe 文件,然后运行它。唯一棘手的部分是您需要修改项目设置 (software.intel.com/en-us/articles/…)。之后运行并查看图表。 ... self time 将告诉您 cpu 在给定函数/类方法中花费了多少时间,不包括对其他函数/方法的调用,而总时间将为您提供花费的总时间在一个函数/方法中,包括调用其他函数/方法所花费的时间。这就像一个计时测试,除了你得到测试中调用的每个函数所花费的时间,包括在 main 中花费的总时间。 您能推荐一个好的、免费 Windows 分析器吗? unix 下的 gprof 之类的? 如果您有 AMD 机器,CodeAnalyst 是免费的。如果你有英特尔,你仍然可以使用它,但只能用于 TBS(基于时间的采样)。【参考方案8】:

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

此外,您应该首先使用std::swap,这完全使整个毫无意义的追求无效。

-1 表示无意义。

【讨论】:

我没有投反对票,但我在 SO 上学到的一件事是,对人放轻松。我们都有不同层次的背景,我们可以分享他人的智慧。显然你有智慧可以分享。这对 SO 来说是件好事。 迈克:点了。你比我更有耐心。除此之外,您认为这个问题有效吗?我很快了解到这里很少有明智的问题。仅优化问题就让人担心这些人正在编写什么应用程序。我希望我的银行不会雇用程序员来怀疑他们是否应该推出自己的 std::swap! :) ufotds:那又怎样?在我的帖子中用“std::reverse”替换“std::swap”。 OP 应该使用 std::whatever 而不是担心自己滚动,担心自己是否更快,并且 - 没有意义的部分 - 避免使用分析器。 考虑到程序员在包括性能在内的许多主题上的愚蠢想法,应用程序运行得和它们一样好,我感到很惊讶。 @John “仅优化问题就让人担心这些人正在编写什么应用程序。”并非所有程序员都是来自 7G 部门的低级别工人,他们在大型团队中担任编码无人机,为无聊的工业规模雇主提供无聊的后端应用程序,并且必须遵守相应的问题。

以上是关于在没有分析器的情况下在 C++ 中测试代码速度的最佳方法,或者尝试没有意义?的主要内容,如果未能解决你的问题,请参考以下文章

是否可以在没有动态多态性的情况下在 C++ 中实现状态设计模式?

在没有 new 的情况下在 C++ 中调用构造函数

在没有 root 权限的情况下使用 C++ 分析多线程代码

在没有向量、指针的情况下在 C++ 中在运行时增加数组大小 [关闭]

如何在没有静态文本的情况下在 XCTest 中快速测试 Webview 是不是加载

如何在没有正则表达式的情况下在 C++ 中实现有效的全字字符串替换?