我需要一种更好的基准代码测试方法,因为好吧,每次都需要重写相同的基准代码只是...好...令人不快。

所以,这是一个仅用于那它会在特定的回合数内运行Action,并在其上计算某些统计信息。因此,您可以为rounds提供任何值,它应该可以工作。 (未经测试的rounds值大于10,000,000。)

最新版本在GitHub上。

非常简单。在运行基准测试的简单类上有两个static方法。该类的优点是它还包括Func<T>的版本,该版本还将验证函数的输出。这意味着您可以同时进行基准测试和验证代码,以确保没有奇怪的事情发生。



/// <summary>
/// Represents the result of a benchmarking session.
/// </summary>
public class BenchmarkResult
{
    /// <summary>
    /// The total number of rounds ran.
    /// </summary>
    public ulong RoundsRun { get; set; }

    /// <summary>
    /// The average time for all the rounds.
    /// </summary>
    public TimeSpan AverageTime { get; set; }

    /// <summary>
    /// The maximum time taken for a single round.
    /// </summary>
    public TimeSpan MaxTime { get; set; }

    /// <summary>
    /// The minimum time taken for a single round.
    /// </summary>
    public TimeSpan MinTime { get; set; }

    /// <summary>
    /// The variance (standard deviation) of all the rounds.
    /// </summary>
    public TimeSpan Variance { get; set; }

    /// <summary>
    /// The number of rounds that passed testing. (Always equivalent to <see cref="RoundsRun"/> for <see cref="Benchmark(ulong, Action)"/>.)
    /// </summary>
    public ulong RoundsPassed { get; set; }

    /// <summary>
    /// The total amount of time taken for all the benchmarks. (Does not include statistic calculation time, or result verification time.)
    /// </summary>
    /// <remarks>
    /// Depending on the number of rounds and time taken for each, this value may not be entirely representful of the actual result, and may have rounded over. It should be used with caution on long-running methods that are run for long amounts of time, though that likely won't be a problem as that would result in the programmer having to wait for it to run. (It would take around 29,247 years for it to wrap around.)
    /// </remarks>
    public TimeSpan TotalTime { get; set; }

    /// <summary>
    /// Runs a benchmark of a method.
    /// </summary>
    /// <param name="rounds">The number of rounds to run.</param>
    /// <param name="method">The code to run.</param>
    /// <returns>A <see cref="BenchmarkResult"/> representing the result of the session.</returns>
    public static BenchmarkResult Benchmark(ulong rounds, Action method)
    {
        var sw = new Stopwatch();

        double m2 = 0;
        double averageTicks = 0;
        double totalValues = 0;
        long maxTicks = 0;
        long minTicks = 0;
        long totalTime = 0;

        for (ulong i = 0; i < rounds; i++)
        {
            sw.Start();
            method.Invoke();
            sw.Stop();

            if (totalValues == 0)
            {
                maxTicks = sw.ElapsedTicks;
                minTicks = sw.ElapsedTicks;
            }

            totalValues++;

            maxTicks = Math.Max(sw.ElapsedTicks, maxTicks);
            minTicks = Math.Min(sw.ElapsedTicks, minTicks);

            // We need to store `delta` here as the `averageTicks` will change on the next calculation, and we need this previous `delta` for the calculation after it.
            double delta = sw.ElapsedTicks - averageTicks;
            averageTicks = averageTicks + delta / totalValues;
            m2 += delta * (sw.ElapsedTicks - averageTicks);

            totalTime += sw.ElapsedTicks;

            sw.Reset();
        }

        double variance = m2 / (totalValues - 1);

        return new BenchmarkResult
        {
            AverageTime = new TimeSpan(Convert.ToInt64(averageTicks)),
            MaxTime = new TimeSpan(maxTicks),
            MinTime = new TimeSpan(minTicks),
            RoundsPassed = rounds,
            RoundsRun = rounds,
            TotalTime = new TimeSpan(totalTime),
            Variance = new TimeSpan(Convert.ToInt64(variance))
        };
    }

    /// <summary>
    /// Runs a benchmark of a function and returns the results of the session.
    /// </summary>
    /// <typeparam name="T">The type of the output of the function.</typeparam>
    /// <param name="rounds">The number of rounds to run.</param>
    /// <param name="method">The code to run.</param>
    /// <param name="expectedResult">The expected result of the function. This will be compared to the actual result and used for <see cref="BenchmarkResult.RoundsPassed"/>. This uses the default <code>object.Equals(object)</code> method.</param>
    /// <returns>A <see cref="BenchmarkResult"/> representing the result of the session.</returns>
    public static BenchmarkResult Benchmark<T>(ulong rounds, Func<T> method, T expectedResult)
    {
        var sw = new Stopwatch();

        double m2 = 0;
        double averageTicks = 0;
        double totalValues = 0;
        long maxTicks = 0;
        long minTicks = 0;
        long totalTime = 0;
        ulong roundsPassed = 0;

        for (ulong i = 0; i < rounds; i++)
        {
            sw.Start();
            var result = method.Invoke();
            sw.Stop();

            if (expectedResult.Equals(result))
            {
                roundsPassed++;
            }

            if (totalValues == 0)
            {
                maxTicks = sw.ElapsedTicks;
                minTicks = sw.ElapsedTicks;
            }

            totalValues++;

            maxTicks = Math.Max(sw.ElapsedTicks, maxTicks);
            minTicks = Math.Min(sw.ElapsedTicks, minTicks);

            // We need to store `delta` here as the `averageTicks` will change on the next calculation, and we need this previous `delta` for the calculation after it.
            double delta = sw.ElapsedTicks - averageTicks;
            averageTicks = averageTicks + delta / totalValues;
            m2 += delta * (sw.ElapsedTicks - averageTicks);

            totalTime += sw.ElapsedTicks;

            sw.Reset();
        }

        double variance = m2 / (totalValues - 1);

        return new BenchmarkResult
        {
            AverageTime = new TimeSpan(Convert.ToInt64(averageTicks)),
            MaxTime = new TimeSpan(maxTicks),
            MinTime = new TimeSpan(minTicks),
            RoundsPassed = roundsPassed,
            RoundsRun = rounds,
            TotalTime = new TimeSpan(totalTime),
            Variance = new TimeSpan(Convert.ToInt64(variance))
        };
    }
}



这将对Thread.Sleep(0)方法进行10,000,000次基准测试,对return true方法进行10,000,000次基准,同时验证它始终返回true。 >

评论

实际上,基准测试的问题不是计算方差而是减少方差,尤其是通过检测和消除以下因素引起的异常值:初始对齐,从磁盘到内存的大量页面故障,任务切换等...对于像大麻这样的语言,您还需要驯服垃圾收集器。同样,平均会破坏一些信息,如果您使用中位数选择之类的信息,这些信息将仍然可用。它混合了所有内容,直至您无法知道信任数据的程度。

你见过这个吗? BenchmarkDotNet

@EBrown:拥有一个罐装解决方案(如您提出的解决方案)可以使人们更有可能不加批判地使用结果,而无需考虑其有效性或计算方案的适当性。在现代文化中,未反映,未批判地使用别人抛出的简单结果似乎有些地方流行。我同样可以肯定的是,一个像您这样经验丰富的程序员看到如此出色的课程的年轻帕达万根本不会考虑基础知识。

@Mast:= 8O在该主题中,任何地方都没有恶意-我的评论仅是为了使事物更直观,以提供更平衡的视图……而我将其评论而不是完整评论,因为它太小了。通过讨论,它以某种方式被炸毁了。

看看BenchmarkDotNet。他们似乎已经考虑了一切,如果没有,我确定他们会欢迎反馈以推动未来的改进。

#1 楼

在进入大问题之前,请先讨论一些小问题:


,请将这些设置者设为私有。此代码的调用者无需更改任何这些值。
除非您与非托管代码进行互操作,否则请不要使用ulonglong有足够的范围。 .NET即使在逻辑上总是正数也使用带符号的数量。这样可以更轻松地进行诸如取两个数量之差的事情。


实际上,基准测试很困难。我为此在一个开发人员站点上撰写了许多文章,但不幸的是,我认为这已经破产了。我必须看看是否可以找到它们并重新发布它们。

假设被测试的代码分配了很多对象,但是没有那么多对象被GC调用。然后,您在同一过程中运行第二个测试,该测试分配了少量的对象,刚好足以将收集压力推高,从而使GC运行起来。谁负责支付该GC的费用?第二次测试。那个GC的大部分成本由谁来负责?第一次测试。因此,您可以轻松地将GC成本计入错误的事件中。 ,具体取决于堆在任何特定时间的状态。

现在,有人可能会说,好吧,我们将在之后执行强制收集(不要忘记等待未决的终结器!)每次测试,并向测试中的代码收取费用。这肯定会消除测试中的变异性,这很好。但是现在,我们正在一个不现实的环境中对代码进行基准测试:一个GC已完全收集的环境。运行您的代码的实际用户将不会在那个世界中运行,因此现在我们通过降低测试的真实性来减少测试的可变性。这种可变性是用户代码体验的一部分,您出于测试目的而将其删除。

我没有一个适合所有人的好的解决方案。我所拥有的知识是,当我设计基准测试时,我必须考虑诸如GC之类的递延成本,并制定如何衡量这些基准的政策。

评论


\ $ \ begingroup \ $
由于这些值只需要设置一次,所以我什至倾向于创建一个私有构造函数并使这些属性不可变。
\ $ \ endgroup \ $
–丹·里昂斯
16年4月13日在17:14

\ $ \ begingroup \ $
@EricLippert看到那些文章会很有趣,我现在很好奇。
\ $ \ endgroup \ $
– Der Kommissar
16年4月13日在18:15

\ $ \ begingroup \ $
关于GC的成本:我认为将这种解释留给开发人员是值得的。如果库在基准测试后清理,但包含分配,发电量分配等内容,则可以解决此问题。您不再是在寻找纯粹的基于时间的基准,而是越多的信息,总会越好。
\ $ \ endgroup \ $
– Jeroen Vannevel
16年4月13日在18:41

\ $ \ begingroup \ $
@SirPython:好的,这是一个复杂的问题。首先:请记住,第一次运行代码时,您要付出使代码抖动的代价,因此它的成本已经与其他任何运行的成本有所不同。这种影响可能比运行GC的影响大得多。其次,在C#中可以强制执行GC,并且可以强制当前线程等待终结器线程完成。
\ $ \ endgroup \ $
–埃里克·利珀特
16年4月14日在0:02

\ $ \ begingroup \ $
@Ebrown在这里看到第一部分:web.archive.org/web/20130607115719/http://tech.pro/blog/1293/…
\ $ \ endgroup \ $
– Heslacher
16年4月14日在5:13

#2 楼

这种方法的问题在于,您无需考虑切换处理器/内核会花费一些时间,而这会伪造结果。可以通过将ProcessorAffinity设置为仅使用特定的处理器/内核来避免这种情况,这样

Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(2); // Use only the second core 


为确保获得该内核的优先级,我们需要设置PriorityClass当前进程的优先级为High,当前线程的优先级为Highest,例如

Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
Thread.CurrentThread.Priority = ThreadPriority.Highest;  

阶段以稳定结果。这意味着调用所需的Action / Function<T>,直到时序似乎相同为止。这在计算机之间是不同的,因此您应该评估自己需要多长时间。为了安全起见,我通常会使用5秒钟的预热时间。

有关此内容(大部分信息来自何处)的很好阅读,请访问http://www.codeproject.com/Articles/61964/Performance-Tests-Precise-Run-Time-Measurements-wi

评论


\ $ \ begingroup \ $
调用一次函数应该足以使JIT编译它,这就是需要预热阶段的原因。另请参见在C#中对小代码样本进行基准测试的相关问题
\ $ \ endgroup \ $
– BlueRaja-Danny Pflughoeft
16-4-13在20:08



\ $ \ begingroup \ $
并非真正的生产代码将在完全不同的环境中运行(如果您正确地进行了测试)。您隐藏了线程切换和并发的潜在问题。知道您的代码在完全人工环境中的性能没有太大价值,而这种环境在实践中将永远无法再现。保持测试尽可能接近实际情况-您在生产产品,而不是白皮书。性能度量是高度相关的,这就是为什么对性能进行猜测非常不可靠的主要原因之一。
\ $ \ endgroup \ $
–罗安
16年4月15日在10:20

#3 楼

您那里有一些非常差的变量名。例如,m2应该是什么意思?


我不喜欢这个事实,该结果会自动产生。我宁愿有两个单独的类:一个运行基准测试,一个代表基准测试结果。与调用Benchmark.Run相比,调用某些BenchmarkResult.Benchmark方法在语义上也更有意义。您的第一种方法是第二种方法的特殊情况,因此,消除代码重复的一种方法是调用:

如果基准测试做任何有意义的工作,考虑到基准测试一开始的准确性就不会真正影响结果。但这可能是您可能想要...进行基准测试的内容。

评论


\ $ \ begingroup \ $
变量m2是该公式的结果的名称。 (请参阅:en.wikipedia.org/wiki/…)
\ $ \ endgroup \ $
– Der Kommissar
16年4月14日在19:17