草庐IT

c# - 我测量运行时间的方法有缺陷吗?

coder 2024-05-20 原文

对不起,它很长,但我只是在分析这个时解释我的思路。问题在最后。

我了解测量代码运行时间的内容。它运行多次以获得平均运行时间,以解释每次运行的差异,并获得更好地利用缓存的时间。

为了测量某人的运行时间,我想出了 this多次修改后的代码。

最后我得到了这段代码,它产生了我打算捕获的结果,而不会给出误导性的数字:

// implementation C
static void Test<T>(string testName, Func<T> test, int iterations = 1000000)
{
    Console.WriteLine(testName);
    Console.WriteLine("Iterations: {0}", iterations);
    var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList();
    var timer = System.Diagnostics.Stopwatch.StartNew();
    for (int i = 0; i < results.Count; i++)
    {
        results[i].Start();
        test();
        results[i].Stop();
    }
    timer.Stop();
    Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds), timer.ElapsedMilliseconds);
    Console.WriteLine("Ticks:    {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks), timer.ElapsedTicks);
    Console.WriteLine();
}

在我见过的所有测量运行时间的代码中,它们通常采用以下形式:
// approach 1 pseudocode
start timer;
loop N times:
    run testing code (directly or via function);
stop timer;
report results;

This was good in my mind since with the numbers, I have the total running time and can easily work out the average running time and would have good cache locality.

But one set of values that I thought were important to have were minimum and maximum iteration running time. This could not be calculated using the above form. So when I wrote my testing code, I wrote them in this form:

// approach 2 pseudocode
loop N times:
    start timer;
    run testing code (directly or via function);
    stop timer;
    store results;
report results;

This is good because I could then find the minimum, maximum as well as average times, the numbers I was interested in. Until now I realized that this could potentially skew results since the cache could potentially be affected since the loop wasn't very tight giving me less than optimal results.


The way I wrote the test code (using LINQ) added additional overheads which I knew about but ignored since I was just measuring the running code, not the overheads. Here was my first version:

// implementation A
static void Test<T>(string testName, Func<T> test, int iterations = 1000000)
{
    Console.WriteLine(testName);
    var results = Enumerable.Repeat(0, iterations).Select(i =>
    {
        var timer = System.Diagnostics.Stopwatch.StartNew();
        test();
        timer.Stop();
        return timer;
    }).ToList();
    Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8}", results.Min(t => t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds));
    Console.WriteLine("Ticks:    {0,3}/{1,10}/{2,8}", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks));
    Console.WriteLine();
}

在这里,我认为这很好,因为我只是在测量运行测试功能所需的时间。与 LINQ 相关的开销不包括在运行时间中。为了减少在循环内创建计时器对象的开销,我进行了修改。
// implementation B
static void Test<T>(string testName, Func<T> test, int iterations = 1000000)
{
    Console.WriteLine(testName);
    Console.WriteLine("Iterations: {0}", iterations);
    var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList();
    results.ForEach(t =>
    {
        t.Start();
        test();
        t.Stop();
    });
    Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds), results.Sum(t => t.ElapsedMilliseconds));
    Console.WriteLine("Ticks:    {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks), results.Sum(t => t.ElapsedTicks));
    Console.WriteLine();
}

这改善了整体时间,但导致了一个小问题。我通过添加每次迭代的时间在报告中添加了总运行时间,但给出了误导性的数字,因为时间很短并且没有反射(reflect)实际运行时间(通常要长得多)。我现在需要测量整个循环的时间,所以我离开了 LINQ 并以我现在在顶部的代码结束。这种混合动力以最小的开销获得了我认为重要的时间 AFAIK。 (启动和停止计时器只是查询高分辨率计时器)此外,发生的任何上下文切换对我来说都不重要,因为它无论如何都是正常执行的一部分。

有一次,我强制线程在循环内让步,以确保在方便的某个时间点给它机会(如果测试代码受 CPU 限制并且根本没有阻塞)。我不太担心正在运行的进程可能会更糟地更改缓存,因为无论如何我都会单独运行这些测试。但是,我得出的结论是,对于这种特殊情况,没有必要。尽管如果证明总体上是有益的,我可能会将其纳入最终的最终版本。也许作为某些代码的替代算法。

现在我的问题:
  • 我做了一些正确的选择吗?有些不对?
  • 我在思考过程中是否对目标做出了错误的假设?
  • 最小或最大运行时间是否真的是有用的信息,还是一个失败的原因?
  • 如果是这样,一般哪种方法会更好?循环运行的时间(方法1)?或者只运行有问题的代码的时间(方法 2)?
  • 我的混合方法一般可以使用吗?
  • 我应该屈服(出于最后一段中解释的原因)还是对时代的伤害比必要的要大?
  • 有没有我没有提到的更优选的方法来做到这一点?

  • 为了清楚起见,我是 不是 寻找一个通用的、随处使用的、准确的计时器。我只是想知道一种算法,当我想要一个快速实现、相当准确的计时器来测量代码时,我应该使用一种算法,当库或其他 3rd 方工具不可用时。

    如果没有异议,我倾向于以这种形式编写所有测试代码:
    // final implementation
    static void Test<T>(string testName, Func<T> test, int iterations = 1000000)
    {
        // print header
        var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList();
        for (int i = 0; i < 100; i++) // warm up the cache
        {
            test();
        }
        var timer = System.Diagnostics.Stopwatch.StartNew(); // time whole process
        for (int i = 0; i < results.Count; i++)
        {
            results[i].Start(); // time individual process
            test();
            results[i].Stop();
        }
        timer.Stop();
        // report results
    }
    

    对于赏金,我希望以上所有问题都能得到解答。我希望得到一个很好的解释,说明我的想法是否对这里的代码产生了很好的影响(以及如果不理想,可能会考虑如何改进它),或者如果我在某一点上有错,请解释为什么它是错误的和/或不必要的,如果适用,提供更好的选择。

    总结重要问题和我对所做决定的想法:
  • 获得每个单独迭代的运行时间通常是一件好事吗?
    通过每次迭代的时间,我可以计算额外的统计信息,例如最小和最大运行时间以及标准偏差。所以我可以看看是否有诸如缓存或其他未知因素之类的因素可能会扭曲结果。这导致了我的“混合”版本。
  • 在实际计时开始之前有一个小的运行循环是否也很好?
    来自我对 Sam Saffron's 的回复在循环中思考,这是为了增加缓存不断访问的内存的可能性。这样我只测量所有内容都被缓存的时间,而不是一些没有缓存内存访问的情况。
  • 会强制Thread.Yield()在循环中帮助还是损害了 CPU 绑定(bind)测试用例的时间?
    如果进程受 CPU 限制,操作系统调度程序将降低此任务的优先级,可能会由于 CPU 时间不足而增加次数。如果它不受 CPU 限制,我将省略屈服。


  • 根据这里的答案,我将使用最终实现编写我的测试函数,而没有针对一般情况的个别时间。如果我想要其他统计数据,我会将其重新引入测试函数并应用此处提到的其他内容。

    最佳答案

    我的第一个想法是一个简单的循环

    for (int i = 0; i < x; i++)
    {
        timer.Start();
        test();
        timer.Stop();
    }
    

    与以下相比有点愚蠢:
    timer.Start();
    for (int i = 0; i < x; i++)
        test();
    timer.Stop();
    

    原因是(1)这种“for”循环的开销非常小,小到即使 test() 只需要一微秒也几乎不值得担心,以及 (2) timer.Start() 和 timer .Stop() 有自己的开销,这可能比 for 循环对结果的影响更大。也就是说,我看了一下 Reflector 中的 Stopwatch 并注意到 Start() 和 Stop() 相当便宜(考虑到所涉及的数学,调用 Elapsed* 属性可能更昂贵。)

    确保 Stopwatch 的 IsHighResolution 属性为 true。如果它是假的,秒表使用 DateTime.UtcNow,我相信它只每 15-16 毫秒更新一次。

    1. 获得每个单独迭代的运行时间通常是一件好事吗?

    通常不需要测量每个单独迭代的运行时间,但它有助于找出不同迭代之间的性能差异。为此,您可以计算最小值/最大值(或 k 个异常值)和标准偏差。只有“中位数”统计要求您记录每次迭代。

    如果您发现标准偏差很大,那么您可能有理由记录每次迭代,以探索为什么时间不断变化。

    有些人编写了一些小框架来帮助您进行性能基准测试。例如,CodeTimers .如果您正在测试的东西如此微小和简单以至于基准库的开销很重要,请考虑在基准库调用的 lambda 内的 for 循环中运行该操作。如果操作太小以至于 for 循环的开销很重要(例如测量乘法的速度),则使用手动循环展开。但是,如果您使用循环展开,请记住,大多数实际应用程序不使用手动循环展开,因此您的基准测试结果可能会夸大实际性能。

    对于我自己,我编写了一个用于收集最小值、最大值、平均值和标准差的小类,可用于基准测试或其他统计数据:
    // A lightweight class to help you compute the minimum, maximum, average
    // and standard deviation of a set of values. Call Clear(), then Add(each
    // value); you can compute the average and standard deviation at any time by 
    // calling Avg() and StdDeviation().
    class Statistic
    {
        public double Min;
        public double Max;
        public double Count;
        public double SumTotal;
        public double SumOfSquares;
    
        public void Clear()
        {
            SumOfSquares = Min = Max = Count = SumTotal = 0;
        }
        public void Add(double nextValue)
        {
            Debug.Assert(!double.IsNaN(nextValue));
            if (Count > 0)
            {
                if (Min > nextValue)
                    Min = nextValue;
                if (Max < nextValue)
                    Max = nextValue;
                SumTotal += nextValue;
                SumOfSquares += nextValue * nextValue;
                Count++;
            }
            else
            {
                Min = Max = SumTotal = nextValue;
                SumOfSquares = nextValue * nextValue;
                Count = 1;
            }
        }
        public double Avg()
        {
            return SumTotal / Count;
        }
        public double Variance()
        {
            return (SumOfSquares * Count - SumTotal * SumTotal) / (Count * (Count - 1));
        }
        public double StdDeviation()
        {
            return Math.Sqrt(Variance());
        }
        public Statistic Clone()
        {
            return (Statistic)MemberwiseClone();
        }
    };
    

    2. 在实际计时开始之前进行一小段运行是否也不错?

    您测量的迭代次数取决于您是最关心启动时间、稳态时间还是总运行时间。通常,将一个或多个运行单独记录为“启动”运行可能很有用。您可以期望第一次迭代(有时不止一次)运行得更慢。作为一个极端的例子,我的 GoInterfaces库始终需要大约 140 毫秒来生成它的第一个输出,然后在大约 15 毫秒内又完成了 9 个输出。

    根据基准测试的测量结果,您可能会发现如果在重新启动后立即运行基准测试,第一次迭代(或前几次迭代)将运行得非常缓慢。然后,如果您第二次运行基准测试,第一次迭代会更快。

    3. 循环中的强制 Thread.Yield() 会帮助还是损害 CPU 绑定(bind)测试用例的计时?

    我不知道。它可能会清除处理器缓存(L1、L2、TLB),这不仅会降低整体基准测试速度,还会降低测量速度。您的结果将更加“人为”,不能很好地反射(reflect)您在现实世界中得到的结果。也许更好的方法是避免在执行基准测试的同时运行其他任务。

    关于c# - 我测量运行时间的方法有缺陷吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/4001610/

    有关c# - 我测量运行时间的方法有缺陷吗?的更多相关文章

    1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

      我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

    2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

      总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

    3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

      类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

    4. ruby - Facter::Util::Uptime:Module 的未定义方法 get_uptime (NoMethodError) - 2

      我正在尝试设置一个puppet节点,但ruby​​gems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由ruby​​gems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby

    5. Ruby 方法() 方法 - 2

      我想了解Ruby方法methods()是如何工作的。我尝试使用“ruby方法”在Google上搜索,但这不是我需要的。我也看过ruby​​-doc.org,但我没有找到这种方法。你能详细解释一下它是如何工作的或者给我一个链接吗?更新我用methods()方法做了实验,得到了这样的结果:'labrat'代码classFirstdeffirst_instance_mymethodenddefself.first_class_mymethodendendclassSecond使用类#returnsavailablemethodslistforclassandancestorsputsSeco

    6. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

      在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

    7. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

      我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

    8. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

      exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

    9. ruby - 无法运行 Rails 2.x 应用程序 - 2

      我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

    10. ruby - Highline 询问方法不会使用同一行 - 2

      设置:狂欢ruby1.9.2高线(1.6.13)描述:我已经相当习惯在其他一些项目中使用highline,但已经有几个月没有使用它了。现在,在Ruby1.9.2上全新安装时,它似乎不允许在同一行回答提示。所以以前我会看到类似的东西:require"highline/import"ask"Whatisyourfavoritecolor?"并得到:Whatisyourfavoritecolor?|现在我看到类似的东西:Whatisyourfavoritecolor?|竖线(|)符号是我的终端光标。知道为什么会发生这种变化吗? 最佳答案

    随机推荐