65.9K
CodeProject 正在变化。 阅读更多。
Home

LINQ 性能测试:我的第一个 Visual Studio 2008 项目

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.52/5 (8投票s)

2007 年 12 月 7 日

CPOL

6分钟阅读

viewsIcon

148931

downloadIcon

197

一个示例 Visual Studio 2008 项目, 将 LINQ 的性能与简单的循环进行比较

引言

本文将讨论 LINQ 循环和常规 For 循环之间的性能差异,以此来初次实践使用 Visual Studio 2008、Linq 和单元测试。

背景

LINQ(语言集成查询)是微软为 .NET 语言新增的功能,允许使用类似 SQL 的语法来构造查询。它在遍历数据集、XML DOM 树和集合时特别有用。

我第一次接触 Visual Studio 2008,特别是 LINQ,是在今年的奥兰多 Tech Ed 大会上。

一位来自微软的出色开发人员为我描述并演示了它(当时它只适用于 VB.NET,但 C# 版本随 beta 2 一同发布)。那位开发人员坚称,LINQ 不仅适用于数据集或复杂对象,还可以用于简单的循环。

我决定将他的理论付诸实践,以此来学习这项新技术:我写了一个简单的程序,用于在数组中搜索奇数,并比较了常规循环和 Linq 循环找出正确答案所花费的时间。

虽然这篇文章是几个月前写的,但我一直等到 VS 2008 和 .NET 3.5 的 RTM 版本发布后才发表。

在此过程中,我更深入地学习了 LINQ 并使用了 VS 2008 的单元测试功能。

随着项目的发展,我加入了第三种循环(ForEach)。然后我决定将所有结果输出到 CSV 文件中,并在 Excel 中分析结果。

逻辑

  1. 该程序分配一个包含 n 个元素的数组,并用数字填充它。
  2. 然后它对 3 个函数中的每一个都调用 GetAverage
  3. GetAverage 会调用每个函数 1000 次(可配置)。
  4. 它测量一个函数遍历 n 个元素所需的时间,并计算平均值。
    测量数据是使用 Daniel Strigl 的高性能系统计时器得出的。
  5. 每个函数针对 n 个元素的平均值都会被显示(或输出到文件)。
  6. 整个代码运行 5 次,以确保平均值的一致性。

Using the Code

这是一个最基础的应用程序。它作为控制台应用程序运行,没有用户界面。
其中唯一可配置的部分是:

  1. numElements - 数组中的元素数量
  2. numIterations - 每个算法被调用的次数,用于计算平均值
  3. 应用程序的输出可以到文件或标准输出(屏幕)——只需注释掉相应的代码行即可。

测试框架

这是 Main 函数

static void Main(string[] args)
{
  StreamWriter file = new StreamWriter("results.txt");
  file.WriteLine("Elements\tFor loop\tForEach Loop\tLinq Loop");
  //Console.WriteLine("Elements\tFor loop\tForEach Loop\tLinq Loop");
  for (int i = 0; i < 5; i++)
  {
    int numElements = 1000 * (int)Math.Pow(10, i);
    FillArray(numElements);
    file.WriteLine("{0:#,#}\t{1:0.0000000000}\t{2:0.0000000000}\t{3:0.0000000000}", 
      numElements, GetAverage(GetOdd), GetAverage(GetOddForEach), GetAverage(GetOddLinq));
    //Console.WriteLine("{0:#,#}\t{1:0.0000000000}\t{2:0.0000000000}\t{3:0.0000000000}", 
      numElements, GetAverage(GetOdd), GetAverage(GetOddForEach), GetAverage(GetOddLinq));
  }
  //Console.ReadLine();
  file.Close();
}

如你所见,它所做的只是调用 GetAverage 函数,并将算法函数作为参数传递。

GetAverage 如下所示

private static double GetAverage(func f)
{
  double averageDuration = 0.0;
  for (int i = 0; i < numIterations; i++)
  {
    pt.Start();
    int odd = f();
    pt.Stop();
    //Console.WriteLine("Time difference: {0}", pt.Duration);
    averageDuration += pt.Duration;
  }
  averageDuration /= numIterations;
  return averageDuration;
}

如你所见,并不太复杂:它启动一个计时器,调用函数 f(),停止计时器并累加时间。它这样做 numIterations 次,并返回平均值。我非常喜欢将函数名作为参数提交的想法,因为它抽象了设计,并让我在未来可以基于此进行扩展。

算法

本质上,所有 3 个函数都使用简单的 O(n) 搜索算法:GetOdd 是最直接的。

private static int GetOdd()
{
  int counter = 0;
  for(int n = 0; n < theArray.Length; n++)
  {
    if (theArray[n] % 2 == 1)
    {
      counter++;
    }
  }
  return counter;
}

GetOddForEach

private static int GetOddForEach()
{
  int counter = 0;
  foreach (int n in theArray)
  {
    if (n % 2 == 1)
    {
        counter++;
    }
  }
  return counter;
}

最后是使用新 Linq 语法的 GetOddLinq

private static int GetOddLinq()
{
  var odd = from n in theArray
      where n % 2 == 1
      select n;
  return odd.Count();
}

你首先会注意到新的关键字 var(设计这个的人是不是想到了 JavaScript?)。它定义了一个新的 IEnumerable 集合,该集合将包含 LINQ 查询的结果。查询本身看起来有点像一个反向的 SQL 查询(select 在末尾),但仍然是可读的。

有关 Linq 语法和示例的更多信息,请访问官方 LINQ 项目页面

结果

我已经在几台计算机和虚拟机上运行了这个程序。我在 Windows XP、Vista 和 2008 RC1 上都试过。我尝试在繁忙的机器上运行,或在完全空闲的机器上运行。最后,我测试了调试版本和发布版本。数字可能会变,但趋势保持不变。

Screenshot - resultstable.png

测量单位是秒。E 列显示了 Linq 相对于 For 增加的时间百分比:Fi = (Di - Bi)/Di

当然,一旦你有了原始数据,你就可以随心所欲地分析它,例如生成一个图表。

Screenshot - resultsgraph.png

注意:如前所述,结果相当一致,Linq 在几乎每次测试中都有 75-85% 的开销。但在调试版本中,LINQ 完成任务所需的时间更长,而 For 和 ForEach 基本保持不变。

我唯一的猜测是,LINQ 内置了一些检测机制,以便于调试——因此它在调试版本中更慢。

单元测试

Tech Ed 的大量会议都致力于测试,特别是,在 VS 2008 中添加单元测试是多么容易。的确,这并不需要很长时间。在源代码的任何地方右键单击并选择“创建单元测试...”。一个向导将引导你选择项目中要测试的函数,并最终创建一个测试项目并将其添加到解决方案中。

测试项目已经准备好了正确的引用和一组访问器——允许单元测试函数访问原始类的所有成员——甚至是私有成员。

那么,你如何测试这段代码呢?这是创建数组的函数的单元测试。

/// <summary>
///A test for FillArray
///</summary>
[TestMethod()]
[DeploymentItem("LinqTest.exe")]
public void FillArrayTest()
{
  int n = 10; // TODO: Initialize to an appropriate value
  Program_Accessor.FillArray(n);
  Assert.AreEqual(n, Program_Accessor.theArray.Length);
}

很简单,不是吗?本质上,你正在使用 Program_Accessor 来访问 LinqTest.exe 程序集。在为 n 个元素调用 FillArray 函数后,你断言数组的大小现在应该是 n

现在,让我们测试其中一个搜索函数(所有函数的测试都相同——单元测试不关心函数的内部逻辑,只关心结果)。

/// <summary>
///A test for GetOddLinq
///</summary>
[TestMethod()]
[DeploymentItem("LinqTest.exe")]
public void GetOddLinqTest()
{
  int expected = 1; // In every 2 numbers, one is odd
  int actual;
  Program_Accessor.FillArray(2);
  actual = Program_Accessor.GetOddLinq();
  Assert.AreEqual(expected, actual);
}

这里我取巧了。知道我的数组将用连续的数字填充,我知道我选取的任何 2 个相邻单元格都将包含 1 个奇数。所以,我们构建一个 2 个单元格的数组,填充它,并将从 GetOddLinqTest 返回的奇数数量与预期结果进行比较。在程序的变体中,如果数组是用随机数填充的,你就必须更改此函数,以获得正确的 expected 值。

注意:随机数不会改变测量结果,因为我们总是需要扫描整个数组。

现在,在构建解决方案之前运行所有单元测试(或点击 CTRL+R, A),然后,希望你能看到所有测试都通过了(显示为绿色)。
Screenshot - Unittests.jpg

历史

1.00 版本于 2007年12月5日发布

1.01 版本于 2007年12月14日发布

更新

根据评论中收到的建议,实施了 2 项修正,以提高测量精度。

  1. 根据 Dennis Dollfus 的建议,LINQ 函数现在看起来是这样的
    private static int GetOddLinq()
    {
      //per Dennis Dollfus's suggestion on CodeProject 12/14/2007
      int oddNumbers = theArray.Count(n => n % 2 == 1);
      return oddNumbers;
    }
  2. 根据 kckn4fun 的建议,结果被累积到一个 StringBuilder 中,并且只在最后才写入文件/控制台。新的 Main 函数看起来是这样的
    static void Main(string[] args)
    { 
        //use of StringBuilder to avoid file noise suggested by kckn4fun on 
        //CodeProject 12/14/2007 
        StringBuilder sb = new StringBuilder(); 
        sb.AppendLine("Elements\tFor loop\tForEach Loop\tLinq Loop"); 
        for (int i = 0; i < 5; i++) 
        { 
            int numElements = 1000 * (int)Math.Pow(10, i);
            FillArray(numElements); 
            sb.AppendLine(string.Format(
              "{0:#,#}\t{1:0.0000000000}\t{2:0.0000000000}\t{3:0.0000000000}", 
                numElements, GetAverage(GetOdd), GetAverage(GetOddForEach), 
                GetAverage(GetOddLinq))); 
         } 
         Console.ReadLine();
         WriteFile(sb.ToString());
    }

新的结果如下所示

Screenshot - resultstable2.png

Screenshot - resultsgraph2.png

如你所见,性能略有提升——但趋势依旧。

最后说明

这个程序绝不是对 LINQ 总体性能的详尽分析。我敢肯定它在遍历复杂数据集和 XML DOM 树时的表现要好得多。我从未打算证明任何事情,只是想在新环境中稍作尝试。

请随意使用这个程序及其结果。根据我的设计方式,它更容易接入更复杂的逻辑并仍然获得测量数据。

© . All rights reserved.