【问题标题】:Forward/Backward loop performance analysis前向/后向循环性能分析
【发布时间】:2013-08-23 21:19:22
【问题描述】:

当我发现算法中存在一个有趣的问题时,我一直在调查我们的开发站点上的事件查看器应用程序的一些性能问题。然后我创建了一个简化的测试项目来测试两种不同的算法。该程序基本上使用EventLog 类检索Windows 事件日志,然后将这些日志转换为可查询的EventLogItem 实体。

此操作使用两个不同的循环来执行和计时。第一个(向后)循环从列表中最后一项的索引开始,翻译该项,然后减少索引。方法定义如下:

private static void TranslateLogsUsingBackwardLoop()
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();

    var originalLogs = EventLog.GetEventLogs();
    var translatedLogs = new List<EventLogItem>();

    Parallel.ForEach<EventLog>(originalLogs, currentLog =>
    {
        for (int index = currentLog.Entries.Count - 1; index >= 0; index--)
        {
            var currentEntry = currentLog.Entries[index];

            EventLogItem translatedEntry = new EventLogItem
            {
                MachineName = currentEntry.MachineName,
                LogName = currentLog.LogDisplayName,
                CreatedTime = currentEntry.TimeGenerated,
                Source = currentEntry.Source,
                Message = currentEntry.Message,
                Number = currentEntry.Index,
                Category = currentEntry.Category,
                Type = currentEntry.EntryType,
                InstanceID = currentEntry.InstanceId,
                User = currentEntry.UserName,
            };

            lock (translatedLogs)
            {
                translatedLogs.Add(translatedEntry);
            }
        }
    });

    stopwatch.Stop();

    Console.WriteLine("{0} logs were translated in {1} using backward loop.", translatedLogs.Count, stopwatch.Elapsed);
}

第二个(前向)循环从索引 0 开始并递增索引。这个方法是这样定义的:

private static void TranslateLogsUsingForwardLoop()
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();

    var originalLogs = EventLog.GetEventLogs();
    var translatedLogs = new List<EventLogItem>();

    Parallel.ForEach<EventLog>(originalLogs, currentLog =>
    {
        for (int index = 0; index < currentLog.Entries.Count; index++)
        {
            var currentEntry = currentLog.Entries[index];

            EventLogItem translatedEntry = new EventLogItem
            {
                MachineName = currentEntry.MachineName,
                LogName = currentLog.LogDisplayName,
                CreatedTime = currentEntry.TimeGenerated,
                Source = currentEntry.Source,
                Message = currentEntry.Message,
                Number = currentEntry.Index,
                Category = currentEntry.Category,
                Type = currentEntry.EntryType,
                InstanceID = currentEntry.InstanceId,
                User = currentEntry.UserName,
            };

            lock (translatedLogs)
            {
                translatedLogs.Add(translatedEntry);
            }
        }
    });

    stopwatch.Stop();

    Console.WriteLine("{0} logs were translated in {1} using forward loop.", translatedLogs.Count, stopwatch.Elapsed);
}

以及主要方法:

static void Main(string[] args)
{
    TranslateLogsUsingForwardLoop();
    Console.WriteLine();
    Thread.Sleep(2000);
    TranslateLogsUsingBackwardLoop();
    Console.ReadLine();
}

这是我得到的(多次执行此测试,结果几乎相同):

请注意,我测试的服务器每秒都会记录到事件日志,这就是翻译日志的数量不一样的原因。那么为什么反向循环更快呢?我最初认为这是因为在后向循环算法中,currentLog.Entries.Count 只被评估一次,而在前向循环中,它需要在每次循环迭代时计算并与index 进行比较,但话又说回来,这似乎不对.有什么想法吗?

【问题讨论】:

  • 两个厘米。当计时总是包括老化循环时。在计时开始之前做一些工作,以使处理器预热(正确节流)。您应该在 For 循环之前启动计时器,而不是为赋值运算符计时。
  • 您的测试中有多少条目?您也尝试过常规(顺序)循环吗?

标签: c# for-loop


【解决方案1】:

老问题,在这种情况下这可能不是确切的原因,但是当循环进入 IL 或程序集或您的语言的低级语言恰好是时,会有所不同。在正常的 for 循环中,您至少可以获取计数值,然后在每个循环中将索引变量与索引变量进行比较。在反向循环中,您将计数一次作为起点,然后比较总是针对 0,这更容易比较,编译器甚至可以优化。不过,您的里程可能会有所不同,并且取决于代码的其余部分,差异可能可以忽略不计。但是如果你需要每个时钟周期反向循环很棒。

【讨论】:

    【解决方案2】:

    再次测试 0 与 maxndex 可能效果不大。但是,由于处理器缓存和/或 O/S 页面缓存,此后不久执行 test1 然后 test2 通常会产生影响。您可能会反转 test1/test2 以查看向前是否神奇地变得比向后更快。现代建筑很难进行准确的分析。

    好的,所以在第一次执行时,Backwards 仍然更快。不是我的第一个猜测,但是由于您使用的是 Parallel 和 lock,因此锁定方法与正向和反向循环之间的差异之间可能存在交互。

    也许反向循环恰好与处理器分支预测一起工作得更好(再次可能与并行性、处理器缓存等交互)。

    由于锁定开销,多线程代码中的许多紧密循环与内存管理有奇怪的交互。 -- 多线程解决方案因为锁竞争而变慢的情况并不少见

    您可以尝试在没有并行的情况下向前和向后运行,看看时间是否变得更加均匀 - 但您最多只能确定它与并行交互或锁争用可能/不太可能相关。分析您的代码可能具有启发性,但也可能无法给出明确的答案。对于这种情况,确定的答案可能非常困难(我假设您主要处于好奇/学习模式)。

    【讨论】:

    • 当我使用常规循环而不是并行循环时,我看不出这两种算法之间有太大区别......仍然不太清楚为什么会这样。
    • 确实,这就是答案的关键。如果性能前后基本相同,则几乎可以肯定是与内存管理锁定或其他一些锁定行为的交互。根除交互的确切性质将非常困难,并且取决于您的环境。
    【解决方案3】:

    第一个循环比较慢是因为它是第一个,而不是因为它是向前的。

    缓存

    现代 CPU 缓存数据(在 1 级和 2 级缓存中)。 第一次访问数据时速度较慢,随后访问时速度较快。

       var currentEntry = currentLog.Entries[index];
    

    第一个循环需要更长的时间,因为它从慢速 RAM 加载到 L2 缓存中。

    我希望第二个循环更快,不管它是如何编写的,因为它是从二级缓存加载的。

    列表

    列表是不断扩展的数组。他们从小(容量 4)开始,然后根据需要将容量翻倍。每次重新分配都很慢。

      var translatedLogs = new List<EventLogItem>();
      ...
    
      translatedLogs.Add(translatedEntry);
    

    第一个循环会经常重新分配:4、8、16、32、64​​p>

    第二个循环将减少重新分配的频率:64、128

    因此,您会期望第二个循环(无论其编写方式如何)更快。

    CPU 优化

    奇怪的事情发生了,因为处理器太复杂了。 你不能再像过去那样预测代码速度了:-)

    Why is processing a sorted array faster than an unsorted array?

    【讨论】:

      猜你喜欢
      • 2019-08-10
      • 1970-01-01
      • 2013-07-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多