3

当我注意到算法中的一个有趣问题时,我一直在调查我们开发站点上的事件查看器应用程序的一些性能问题。然后我创建了一个简化的测试项目来测试两种不同的算法。该程序基本上使用该类检索 Windows 事件日志EventLog,然后将这些日志转换为可查询的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在每次循环迭代时计算和比较,但话又说回来,这似乎不对。有任何想法吗?

4

3 回答 3

1

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

于 2016-06-01T15:28:21.297 回答
0

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

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

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

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

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

于 2013-08-23T21:26:18.207 回答
0

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

缓存

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

   var currentEntry = currentLog.Entries[index];

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

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

列表<T>

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

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

  translatedLogs.Add(translatedEntry);

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

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

所以你会期望第二个循环(不管它是如何写的)更快。

CPU 优化

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

为什么处理排序数组比处理未排序数组更快?

于 2016-06-01T15:50:01.240 回答