前向/后向循环性能分析

时间:2013-08-23 21:19:22

标签: c# for-loop

当我注意到算法中有一个有趣的问题时,我一直在调查我们在开发网站上的事件查看器应用程序的一些性能问题。然后我创建了一个简化的测试项目来测试两种不同的算法。该程序基本上使用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();
}

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

enter image description here

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

3 个答案:

答案 0 :(得分:1)

旧问题,在这种情况下,这可能不是确切的原因,但是当循环到达IL或汇编或者你的语言的低级语言恰好是什么时,会有区别。在正常for循环中,您将获得计数值,然后将索引变量与每个循环上的变量进行比较。在反向循环中,您将计数一次作为起点,然后比较始终与0进行比较,这更容易比较,编译器甚至可以进行优化。您的里程可能会有所不同,并且根据代码的其余部分,它可能是微不足道的差异。但如果你需要每个时钟周期,反向循环是很棒的。

答案 1 :(得分:0)

再次测试0与maxndex相比可能影响不大。但是,之后不久执行test1然后test2通常会因处理器缓存和/或O / S页面缓存而产生影响。您可以反转test1 / test2以查看前锋是否神奇地变得比后退更快。现代建筑的精确剖析很难。

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

可能后向循环恰好在处理器分支预测中更好地工作(再次可能与并行性,处理器缓存等交互)。

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

您可以尝试前后不平行运行,看看时间是否变得更加均匀 - 但最多只能确定它与并行交互或锁争用的可能性/可能性不大。对您的代码进行概要分析可能具有启发性,但也可能不会产生明确的答案。对于这种情况,一个确定的答案可能非常困难(我假设你主要处于好奇/学习模式)。

答案 2 :(得分:0)

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

<强>缓存

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

   var currentEntry = currentLog.Entries[index];

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

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

<强>列表&LT; T&GT;

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

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

  translatedLogs.Add(translatedEntry);

第一个循环将经常重新分配:4,8,16,32,64

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

所以你会期望第二个循环(无论其编写方式如何)更快。

CPU优化

由于处理器非常复杂,所以会发生奇怪的事情。 你不能再像过去那样预测代码速度了: - )

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