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

2024-05-13

抱歉,这篇文章很长,但我只是在分析这个问题时解释一下我的思路。问题在最后。

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

为了测量某人的跑步时间,我想出了this https://stackoverflow.com/questions/3992363/sum-of-products-of-two-arrays-dotproduct/3992840#3992840代码经过多次修改。

最后,我得到了这段代码,它产生了我想要捕获的结果,而没有给出误导性的数字:

// 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;
  

这在我看来很好,因为有了这些数字,我就有了总运行时间,并且可以轻松计算出平均运行时间,并且具有良好的缓存局部性。

但我认为重要的一组值是最小和最大迭代运行时间。使用上面的表格无法计算出这一点。所以当我编写测试代码时,我以这种形式编写它们:



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

这很好,因为我可以找到最小、最大和平均时间,以及我感兴趣的数字。直到现在,我意识到这可能会扭曲结果,因为循环不是很紧,因此缓存可能会受到影响给我的结果不是最佳的。


我编写测试代码的方式(使用 LINQ)增加了额外的开销,我知道这些开销,但忽略了,因为我只是测量正在运行的代码,而不是开销。这是我的第一个版本:

// 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();
}

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

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


现在我的问题是:

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

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

如果没有异议,我倾向于以这种形式编写所有测试代码:

// 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
}

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

总结重要的问题和我对所做决定的想法:

  1. 获取每个单独迭代的运行时间通常是一件好事吗?
    通过每次迭代的时间,我可以计算其他统计信息,例如最小和最大运行时间以及标准差。所以我可以看看是否有诸如缓存或其他未知因素之类的因素可能会扭曲结果。这导致了我的“混合”版本。
  2. 在实际计时开始之前进行一个小循环也很好吗?
    从我的回复到萨姆·萨弗隆的 https://stackoverflow.com/questions/4001610/is-my-method-of-measuring-running-time-flawed/4102936#4102936在循环中思考,这是为了增加不断访问的内存被缓存的可能性。这样,我仅测量所有内容都被缓存时的时间,而不是某些内存访问未缓存的情况。
  3. 是否会被迫Thread.Yield()循环内对 CPU 限制测试用例的计时有帮助还是有害?
    如果进程受 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() 和计时器。 Stop() 有自己的开销,这可能比 for 循环对结果的影响更大。也就是说,我看了一下 Reflector 中的 Stopwatch,发现 Start() 和 Stop() 相当便宜(考虑到所涉及的数学,调用 Elapsed* 属性可能更昂贵。)

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

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

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

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

有些人编写了小型框架来帮助您进行性能基准测试。例如,代码定时器 https://web.archive.org/web/20160210021002/http://blogs.msdn.com:80/b/vancem/archive/2006/09/21/765648.aspx。如果您正在测试的东西非常小且简单,以至于基准库的开销很重要,请考虑在基准库调用的 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. 在实际计时开始之前进行小循环也很好吗?

您测量哪些迭代取决于您最关心的是启动时间、稳态时间还是总运行时间。一般来说,将一次或多次运行单独记录为“启动”运行可能很有用。您可以预期第一次迭代(有时不止一次)运行得更慢。举个极端的例子,我的Go接口 https://www.codeproject.com/Articles/87991/Dynamic-interfaces-in-any-NET-language库始终需要大约 140 毫秒来生成第一个输出,然后在大约 15 毫秒内再生成 9 个输出。

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

3. 循环内的强制 Thread.Yield() 是否有助于或损害 CPU 限制测试用例的计时?

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

我测量运行时间的方法有缺陷吗? 的相关文章

  • 结构化绑定中缺少类型信息

    我刚刚了解了 C 中的结构化绑定 但有一件事我不喜欢 auto x y some func is that auto正在隐藏类型x and y 我得抬头看看some func的声明来了解类型x and y 或者 我可以写 T1 x T2 y
  • 类型中的属性名称必须是唯一的

    我正在使用 Entity Framework 5 并且有以下实体 public class User public Int32 Id get set public String Username get set public virtual
  • C++11 删除重写方法

    Preface 这是一个关于最佳实践的问题 涉及 C 11 中引入的删除运算符的新含义 当应用于覆盖继承父类的虚拟方法的子类时 背景 根据标准 引用的第一个用例是明确禁止调用某些类型的函数 否则转换将是隐式的 例如最新版本第 8 4 3 节
  • 从经典 ASP 调用 .Net C# DLL 方法

    我正在开发一个经典的 asp 项目 该项目需要将字符串发送到 DLL DLL 会将其序列化并发送到 Zebra 热敏打印机 我已经构建了我的 DLL 并使用它注册了regasm其次是 代码库这使得 IIS 能够识别它 虽然我可以设置我的对象
  • C++ 多行字符串原始文字[重复]

    这个问题在这里已经有答案了 我们可以像这样定义一个多行字符串 const char text1 part 1 part 2 part 3 part 4 const char text2 part 1 part 2 part 3 part 4
  • ASP.NET Core 3.1登录后如何获取用户信息

    我试图在登录 ASP NET Core 3 1 后获取用户信息 如姓名 电子邮件 id 等信息 这是我在登录操作中的代码 var claims new List
  • C# 列表通用扩展方法与非通用扩展方法

    这是一个简单的问题 我希望 集合类中有通用和非通用方法 例如List
  • 使用 C# 中的 CsvHelper 将不同文化的 csv 解析为十进制

    C 中 CsvHelper 解析小数的问题 我创建了一个从 byte 而不是文件获取 csv 文件的类 并且它工作正常 public static List
  • 如何获取 EF 中与组合(键/值)列表匹配的记录?

    我有一个数据库表 其中包含每个用户 年份组合的记录 如何使用 EF 和用户 ID 年份组合列表从数据库获取数据 组合示例 UserId Year 1 2015 1 2016 1 2018 12 2016 12 2019 3 2015 91
  • C# - 当代表执行异步任务时,我仍然需要 System.Threading 吗?

    由于我可以使用委托执行异步操作 我怀疑在我的应用程序中使用 System Threading 的机会很小 是否存在我无法避免 System Threading 的基本情况 只是我正处于学习阶段 例子 class Program public
  • 实例化类时重写虚拟方法

    我有一个带有一些虚函数的类 让我们假设这是其中之一 public class AClassWhatever protected virtual string DoAThingToAString string inputString retu
  • C# 动态/expando 对象的深度/嵌套/递归合并

    我需要在 C 中 合并 2 个动态对象 我在 stackexchange 上找到的所有内容仅涵盖非递归合并 但我正在寻找能够进行递归或深度合并的东西 非常类似于jQuery 的 extend obj1 obj2 http api jquer
  • 复制目录下所有文件

    如何将一个目录中的所有内容复制到另一个目录而不循环遍历每个文件 你不能 两者都不Directory http msdn microsoft com en us library system io directory aspx nor Dir
  • 如何在 Android 中使用 C# 生成的 RSA 公钥?

    我想在无法假定 HTTPS 可用的情况下确保 Android 应用程序和 C ASP NET 服务器之间的消息隐私 我想使用 RSA 来加密 Android 设备首次联系服务器时传输的对称密钥 RSA密钥对已在服务器上生成 私钥保存在服务器
  • 对于某些 PDF 文件,LoadIFilter() 返回 -2147467259

    我正在尝试使用 Adob e IFilter 搜索 PDF 文件 我的代码是用 C 编写的 我使用 p invoke 来获取 IFilter 的实例 DllImport query dll SetLastError true CharSet
  • 为什么C++代码执行速度比java慢?

    我最近用 Java 编写了一个计算密集型算法 然后将其翻译为 C 令我惊讶的是 C 的执行速度要慢得多 我现在已经编写了一个更短的 Java 测试程序和一个相应的 C 程序 见下文 我的原始代码具有大量数组访问功能 测试代码也是如此 C 的
  • 当文件流没有新数据时如何防止fgets阻塞

    我有一个popen 执行的函数tail f sometextfile 只要文件流中有数据显然我就可以通过fgets 现在 如果没有新数据来自尾部 fgets 挂起 我试过ferror and feof 无济于事 我怎样才能确定fgets 当
  • C# 中最小化字符串长度

    我想减少字符串的长度 喜欢 这串 string foo Lorem ipsum dolor sit amet consectetur adipiscing elit Aenean in vehicula nulla Phasellus li
  • C++ 中的参考文献

    我偶尔会在 StackOverflow 上看到代码 询问一些涉及函数的重载歧义 例如 void foo int param 我的问题是 为什么会出现这种情况 或者更确切地说 你什么时候会有 对参考的参考 这与普通的旧参考有何不同 我从未在现
  • 如何确定 CultureInfo 实例是否支持拉丁字符

    是否可以确定是否CultureInfo http msdn microsoft com en us library system globalization cultureinfo aspx我正在使用的实例是否基于拉丁字符集 我相信你可以使

随机推荐