【问题标题】:How can I test the performance of a C function?如何测试 C 函数的性能?
【发布时间】:2009-11-06 14:58:37
【问题描述】:

是否有一些好方法可以了解函数在 C 中的执行情况?例如,我想将我自己的函数与库函数进行比较。

【问题讨论】:

    标签: c performance function


    【解决方案1】:

    您需要高分辨率的计时器。

    在 Linux 上,@987654321@ 是一个不错的选择,它为您提供微秒级分辨率。在 Windows 上,@987654322@ 是典型的。确保多次运行函数以获得稳定的读数。

    快速示例,适用于 Linux:

    struct timeval t0, t1;
    unsigned int i;
    
    gettimeofday(&t0, NULL);
    for(i = 0; i < 100000; i++)
      function_to_measure();
    gettimeofday(&t1, NULL);
    printf("Did %u calls in %.2g seconds\n", i, t1.tv_sec - t0.tv_sec + 1E-6 * (t1.tv_usec - t0.tv_usec));
    

    您当然会调整计数 (100,000) 以匹配函数的性能。最好是函数确实需要一段时间才能运行,否则循环和/或函数调用开销可能会占主导地位。

    【讨论】:

    • 感谢您的提示和示例。我在这里运行 mac os,所以这里也可以使用 gettimeofday()。
    • 如果函数只依赖于内存和 cpu 并且不改变状态(即每次都运行相同),这可以正常工作。如果您的函数具有文件访问权限,您可能会被文件系统缓存所迷惑。
    【解决方案2】:

    开源的Callgrind 分析器(适用于 Linux)是一种非常棒的性能测量方法。与 KCacheGrind 结合使用,您可以非常直观地了解您的时间花费在哪里。

    Callgrind 是 Valgrind 的一部分。

    • 艺术

    【讨论】:

      【解决方案3】:

      你好,我给你举个例子解释一下:

      #include <stdio.h>
      #include <time.h>
      
      int main(void)
      {
      
          clock_t start_clk = clock();
      
          /*
              put any code here
          */
      
          printf("Processor time used by program: %lg sec.\n", \
          (clock() - start_clk) / (long double) CLOCKS_PER_SEC);
      
          return 0;
      }
      

      输出:程序使用的处理器时间:4.94066e-324 秒。

      时间.h:

      声明 clock_t 是一个算术(你可以像我在示例中那样对这个值进行数学运算)时间值。 基本上把任何代码放在注释所在的地方。

      CLOCKS_PER_SEC是time.h中声明的宏,用它作为分母将值转换为秒。

      必须将其强制转换为 long double 原因有两个:

      1. 我们不知道 clock_t 究竟是什么类型,但我们想打印它(您将在 printf 中进行什么转换?)。
      2. long double 是一种非常精确的类型,可以表示非常小的值。

      【讨论】:

        【解决方案4】:

        运行它(他们)数百万次(每次)并测量它所花费的时间。
        完成得越快,性能越好。

        gprof 可以帮忙:)

        这是我运行我的程序 10 秒时 gprof 的结果(函数名称已更改)

        每个样本计为 0.01 秒。 % 累计自我自我总计 时间 秒 秒 呼叫 ms/呼叫 ms/呼叫名称 60.29 8.68 8.68 115471546 0.00 0.00 工作量 39.22 14.32 5.64 46 122.70 311.32 工作_b 0.49 14.39 0.07 内联 0.07 14.40 0.01 46 0.22 0.22 工作_c 0.00 14.40 0.00 460 0.00 0.00 find_minimum 0.00 14.40 0.00 460 0.00 0.00 反馈 0.00 14.40 0.00 46 0.00 0.00 work_a

        【讨论】:

        • 一般同意这一点。但是,由于缓存问题,第一次迭代可能会比其他迭代慢很多。如果例程通常只执行一次,而不是在一个紧密的循环中,这会给你一个扭曲的画面。 OTOH,如果例程只执行一次,您也不应该浪费宝贵的时间来尝试分析或优化它。
        • 谢谢pmg,我去看看gprof。我注意到我什至默认安装了它。
        • T.E.D.提出了几个很好的观点。操作系统的 CPU 缓存和缓存将大大提高除第一次迭代之外的所有功能的性能,使您的平均性能远远超过单独运行或在其他功能之间运行时获得的性能,足以取代CPU 缓存的内容。但这可能是目前最好的简单分析技术,并且仍然会给你一个大致良好/可接受/糟糕的性能数据。
        • 如果你混淆了电话,你会倾向于避免 T.E.D. 的影响。注意到。另一方面,缓存会影响所有的功能,而且效果可能会变平。
        • 是的,但是对于许多后续调用来说,性能不是最重要的。将每次迭代中的紧密循环与函数调用与单个调用进行比较。在我看来,单个呼叫的性能不太重要。
        【解决方案5】:

        Fred,我注意到您在评论中说您使用的是 OS X。在 OS X 上获得非常准确的小规模函数计时的最佳方法是使用 mach_absoute_time( ) 函数。您可以按如下方式使用它:

        #include <mach/mach_time.h>
        #include <stdint.h>
        
        int loopCount;
        
        uint64_t startTime = mach_absolute_time( );
        for (loopCount = 0; loopCount < iterations; ++loopCount) {
            functionBeingTimed( );
        }
        uint64_t endTime = mach_absolute_time( );
        double averageTime = (double)(endTime-startTime) / iterations;
        

        这为您提供了对函数的 iterations 调用的平均时间。这可能会受到系统进程之外的影响的影响。因此,您可能希望花费最快的时间:

        #include <mach/mach_time.h>
        #include <stdint.h>
        
        int loopCount;
        
        double bestTime = __builtin_inf();
        for (loopCount = 0; loopCount < iterations; ++loopCount) {
            uint64_t startTime = mach_absolute_time( );
            functionBeingTimed( );
            uint64_t endTime = mach_absolute_time( );
            double bestTime = __builtin_fmin(bestTime, (double)(endTime-startTime));
        }
        

        这可能有其自身的问题,特别是如果正在计时的功能非常非常快。您需要考虑您真正想要测量的内容并选择一种科学合理的方法(良好的实验设计困难)。我经常使用这两种方法之间的混合作为衡量新任务(多次调用的最低平均值)的第一次尝试。

        另请注意,在上面的代码示例中,时间以“马赫时间单位”为单位。如果您只想比较算法,这通常很好。出于某些其他目的,您可能希望将它们转换为纳秒或周期。为此,您可以使用以下函数:

        #include <mach/mach_time.h>
        #include <sys/sysctl.h>
        #include <stdint.h>
        
        double ticksToNanoseconds(double ticks) {
            static double nanosecondsPerTick = 0.0;
            // The first time the function is called
            // ask the system how to convert mach
            // time units to nanoseconds
            if (0.0 == nanosecondsPerTick) {
                mach_timebase_info_data_t timebase;
                // to be completely pedantic, check the return code of this call:
                mach_timebase_info(&timebase);
                nanosecondsPerTick = (double)timebase.numer / timebase.denom;
            }
            return ticks * nanosecondsPerTick;
        }
        
        double nanosecondsToCycles(double nanoseconds) {
            static double cyclesPerNanosecond = 0.0;
            // The first time the function is called
            // ask the system what the CPU frequency is
            if (0.0 == cyclesPerNanosecond) {
                uint64_t freq;
                size_t freqSize = sizeof(freq);
                // Again, check the return code for correctness =)
                sysctlbyname("hw.cpufrequency", &freq, &freqSize, NULL, 0L );
                cyclesPerNanosecond = (double)freq * 1e-9;
            }
            return nanoseconds * cyclesPerNanosecond;
        }
        

        请注意,转换为纳秒始终是正确的,但转换为周期可能会以各种方式出错,因为现代 CPU 不会以固定的速度运行。尽管如此,它通常工作得很好。

        【讨论】:

        • 谢谢斯蒂芬,太好了!我会试试这个。
        • 如果您遇到任何问题,请告诉我;我从记忆中输入了所有这些,所以我可能在某处犯了错误 =)
        【解决方案6】:

        所有这些其他答案都使用gettimeofday() 的某些变体进行计时。这是相当粗糙的,因为您通常需要多次运行内核才能获得可重现的结果。将其置于紧密循环中会更改代码和数据缓存的状态,因此这些结果可能无法指示实际性能。

        更好的选择是实际使用 CPU 周期计数器。在 x86 上,您可以使用 rdtsc 指令执行此操作。这是来自x264

        static inline uint32_t read_time(void)
        {
            uint32_t a = 0;
        #if defined(__GNUC__) && (defined(ARCH_X86) || defined(ARCH_X86_64))
            asm volatile( "rdtsc" :"=a"(a) ::"edx" );
        #elif defined(ARCH_PPC)
            asm volatile( "mftb %0" : "=r" (a) );
        #elif defined(ARCH_ARM)     // ARMv7 only
            asm volatile( "mrc p15, 0, %0, c9, c13, 0" : "=r"(a) );
        #endif
            return a;
        }
        

        有关使用各种硬件计数器进行分析的更多信息,请参阅PAPI。出于某些目的,模拟器(如 Callgrind 和基于中断的分析器 (Oprofile) 很有用。

        【讨论】:

          【解决方案7】:

          在进入函数之前存储系统时间。 从函数返回后存储系统时间。 减去差异并比较两个实现。

          【讨论】:

          • 当然,循环它的次数足够多,这样你就不会得到零差。
          【解决方案8】:

          查看HighResTimer 以获得高性能计时器。

          您可能会发现存储之前/之后的时间不够准确,并且可能会导致 0,除非您有更长的运行时间。

          【讨论】:

            【解决方案9】:

            查看RDTSC,但最好如下所示。

            0 - 调用系统的 Sleep 或 Yield 函数,以便当它返回时,您有一个新的时间片

            1 - RDTSC

            2 - 调用你的函数

            3 - RDTSC

            如果你的函数是一个长时间运行的函数,你必须使用某种分析工具,比如 gprof(它非常容易使用)和 Intel 的 VTune 应用程序(我很久没用过)。看到 Art 的回答后,我改变了主意,从 gprof 转到 Callgrind。我以前只用过 Valgrind 的 Memcheck 工具,它是一个很棒的工具。我之前没用过Callgrind,但我相信它比gprof 好...

            【讨论】:

            • 这很有趣,我不知道有组装说明。可能也必须尝试一下,看看它是如何工作的。
            【解决方案10】:
            • 存储时间戳进入函数

            • 存储时间戳退出函数

            • 比较时间戳

            确保使用重要的样本,因为时间分辨率可能会改变您的结果。对于短期功能尤其如此。使用高分辨率计时器(大多数平台都提供微秒分辨率)。

            【讨论】:

              【解决方案11】:

              作为最简单和可移植的方法,您可以使用标准函数 time(),它返回自 Epoch 以来的当前秒数。

              
              #include <time.h>
              
              time_t starttime, endtime;
              
              starttime = time(NULL);
              for (i = 0; i < 1000000; i++)
              {
                  testfunc();
              }
              endtime = time(NULL);
              
              printf("Time in seconds is %d\n", (int)(endtime-starttime));
              

              根据您的需要调整迭代次数。如果一个函数调用需要 5 秒,那么你需要一杯咖啡进行 1000000 次迭代......当差异小于 1 秒时,即使对于一个很大的数字,你应该 1)问自己是否重要,如果是,2 ) 检查您最喜欢的编译器是否已经具有内置分析功能。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2018-12-09
                • 1970-01-01
                相关资源
                最近更新 更多