两个等效 LINQ 查询的首次执行总是运行得较慢
First executed of two equivalent LINQ queries always runs slower
考虑以下两种编写此 LINQ 查询的方法:
选项 1:
public void MyMethod(List<MyObject> myList)
{
...
var isValid = myList.Where(l => l.IsActive)
.GroupBy(l => l.Category)
.Select(g => g.Count() > 300) //arbitrary number for the sake of argument
.Any();
}
选项 2:
public void MyMethod(List<MyObject> myList)
{
...
var isValid = myList.Where(l => l.IsActive)
.GroupBy(l => l.Category)
.Select(g => g.Count())
.Any(total => total > 300); //arbitrary number for the sake of argument
}
我想看看两者之间的性能是否有差异,所以我创建了一个控制台应用程序(如下所示)来比较它们。
发生的事情是首先执行的查询总是 运行s 慢,然后在随后的 运行s 它们都显示为 运行 在 0 毫秒内。然后我将比较值更改为 Ticks 并得到类似的结果。如果我切换查询的执行顺序,新的第一个现在 运行s 慢。
所以问题是双重的,为什么第一个执行的查询看起来比较慢?而且,有没有一种方法可以实际比较两者的性能?
测试代码如下:
static void Main(string[] args)
{
Console.WriteLine("Running test");
var rnd = new Random();
for (var i = 0;i < 5; i++)
{
RunTest(i, rnd);
Console.WriteLine();
Console.WriteLine();
}
Console.ReadKey();
}
private static void RunTest(int runId, Random rnd)
{
var list = GetData(rnd);
var startOne = DateTime.Now.TimeOfDay;
var one = list.Where(l => l.IsActive)
.GroupBy(l => l.Category)
.Select(g => g.Count() > 300)
.Any();
var endOne = DateTime.Now.TimeOfDay;
var startTwo = DateTime.Now.TimeOfDay;
var two = list.Where(l => l.IsActive)
.GroupBy(l => l.Category)
.Select(g => g.Count())
.Any(c => c > 300);
var endTwo = DateTime.Now.TimeOfDay;
var resultOne = (endOne - startOne).Milliseconds;
var resultTwo = (endTwo - startTwo).Milliseconds;
Console.WriteLine($"Results for test run #{++runId}");
Console.WriteLine();
Console.WriteLine($"Category 1 total: {list.Where(l => l.Category == 1 && l.IsActive).Count()}");
Console.WriteLine($"Category 2 total: {list.Where(l => l.Category == 2 && l.IsActive).Count()}");
Console.WriteLine($"Category 3 total: {list.Where(l => l.Category == 3 && l.IsActive).Count()}");
Console.WriteLine();
Console.WriteLine($"First option runs in: {resultOne} ");
Console.WriteLine();
Console.WriteLine($"Second option runs in: {resultTwo} ");
}
private static List<MyObject> GetData(Random rnd)
{
var result = new List<MyObject>();
for (var i = 0; i < 1000; i++)
{
result.Add(new MyObject { Category = rnd.Next(1, 4), IsActive = rnd.Next(0, 2) != 0 });
}
return result;
}
}
public class MyObject
{
public bool IsActive { get; set; }
public int Category { get; set; }
}
您的基准测试方法存在几个问题。
首先,当您有两个 DateTime
值并通过它们的 TimeOfDay
属性比较它们时...
var startOne = DateTime.Now.TimeOfDay;
// Do some work
var endOne = DateTime.Now.TimeOfDay;
var resultOne = (endOne - startOne).Milliseconds;
...然后你 运行 如果测试跨越一天 t运行 sition(午夜),你就有 负面 持续时间的风险.考虑一下...
DateTime midnight = DateTime.Today;
DateTime fiveSecondsBeforeMidnight = midnight - TimeSpan.FromSeconds(5);
DateTime fiveSecondsAfterMidnight = midnight + TimeSpan.FromSeconds(5);
Console.WriteLine($"Difference between DateTime values: {fiveSecondsAfterMidnight - fiveSecondsBeforeMidnight}");
Console.WriteLine($"Difference between TimeOfDay values: {fiveSecondsAfterMidnight.TimeOfDay - fiveSecondsBeforeMidnight.TimeOfDay}");
...打印...
Difference between DateTime values: 00:00:10
Difference between TimeOfDay values: -23:59:50
相反,您可以通过直接比较 DateTime
值来修复此错误并简化代码...
var startOne = DateTime.Now;
// Do some work
var endOne = DateTime.Now;
var resultOne = (endOne - startOne).Milliseconds;
这可以进一步改进,但是,通过使用 Stopwatch
class,它比比较 DateTime
值更准确,并且专门为此目的设计...
Stopwatch stopwatch = Stopwatch.StartNew();
// Do some work
TimeSpan resultOne = stopwatch.Elapsed;
stopwatch.Restart();
// Do some work
TimeSpan resultTwo = stopwatch.Elapsed;
二、TimeSpan.Milliseconds
property returns only the milliseconds component of the TimeSpan
value. To get the TimeSpan
value in milliseconds you want the TotalMilliseconds
property。考虑这里的区别...
TimeSpan value1 = TimeSpan.FromSeconds(1) + TimeSpan.FromMilliseconds(500);
TimeSpan value2 = TimeSpan.FromMilliseconds(900);
Console.WriteLine($" value1.Milliseconds: {value1.Milliseconds}");
Console.WriteLine($"value1.TotalMilliseconds: {value1.TotalMilliseconds}");
Console.WriteLine($" value2.Milliseconds: {value2.Milliseconds}");
Console.WriteLine($"value2.TotalMilliseconds: {value2.TotalMilliseconds}");
Console.WriteLine($"value1 is {(value1.Milliseconds < value2.Milliseconds ? "less" : "greater")} than value2 (by Milliseconds)");
Console.WriteLine($"value1 is {(value1.TotalMilliseconds < value2.TotalMilliseconds ? "less" : "greater")} than value2 (by TotalMilliseconds)");
...打印...
value1.Milliseconds: 500
value1.TotalMilliseconds: 1500
value2.Milliseconds: 900
value2.TotalMilliseconds: 900
value1 is less than value2 (by Milliseconds)
value1 is greater than value2 (by TotalMilliseconds)
像您一样比较 Ticks
属性 是解决此问题的另一种方法,或者您可以将时差存储为 TimeSpan
而无需选择其中一个属性并让字符串格式处理较小的组件...
TimeSpan resultOne = endOne - startOne;
TimeSpan resultTwo = endTwo - startTwo;
// ...
Console.WriteLine($"First option runs in: {resultOne:s\.ffffff} seconds");
Console.WriteLine();
Console.WriteLine($"Second option runs in: {resultTwo:s\.ffffff} seconds");
最后,我 运行 你的代码并看到与你所做的相同的结果:第一个 运行 是非零的,随后的 运行 是零。我的猜测是第一个 运行 需要更长的时间,因为您的代码尚未经过 JIT 优化。即使那些 "slow" 第一个 运行 也只需要几毫秒就可以完成,因为您的列表只有一千个项目。做空的基准 运行 无法提供有意义的比较。
进行上述更改并将 GetData()
返回的 List<>
的大小增加到 1000 万个项目后,每个 运行 需要几秒钟,第一个选项是几个在第一个 运行 中快了毫秒,在随后的 运行 中慢了 25-125 毫秒。
您可以考虑使用像 BenchmarkDotNet 这样的库,而不是滚动您自己的基准代码。它会处理一些细节,例如确定要执行多少 运行,"warming up" 您的代码以确保它已经过优化,以及为您计算统计数据。
是的,您可以使用 BenchmarkDotNet 准确比较两个选项的性能。这将成为一个简单的测试脚本来设置。
void Main()
{
var summary = BenchmarkRunner.Run<CollectionBenchmark>();
}
[MemoryDiagnoser]
public class CollectionBenchmark
{
private static Random random = new Random();
private List<MyObject> _list = new List<MyObject>();
[GlobalSetup]
public void GlobalSetup()
{
var rnd = new Random();
for (var i = 0; i < 1000; i++)
{
_list.Add(new MyObject { Category = rnd.Next(1, 4), IsActive = rnd.Next(0, 2) != 0 });
}
}
[Benchmark]
public void OptionOne()
{
var one = _list.Where(l => l.IsActive)
.GroupBy(l => l.Category)
.Select(g => g.Count() > 300)
.Any();
}
[Benchmark]
public void OptionTwo()
{
var two = _list.Where(l => l.IsActive)
.GroupBy(l => l.Category)
.Select(g => g.Count())
.Any(c => c > 300);
}
}
public class MyObject
{
public bool IsActive { get; set; }
public int Category { get; set; }
}
这在我的机器上产生了以下结果:
BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i5-6300U CPU 2.40GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
Frequency=2437498 Hz, Resolution=410.2567 ns, Timer=TSC
[Host] : .NET Framework 4.6.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3324.0
DefaultJob : .NET Framework 4.6.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3324.0
| Method | Mean | Error | StdDev | Gen 0 | Allocated |
|---------- |---------:|----------:|----------:|-------:|----------:|
| OptionOne | 36.73 us | 0.7491 us | 1.9202 us | 8.4839 | 13.13 KB |
| OptionTwo | 36.37 us | 0.6993 us | 0.8053 us | 8.4839 | 13.13 KB |
分配的内存是一样的。考虑到基准测量的时间差为几分之一微秒,两者的性能没有实际差异。
考虑以下两种编写此 LINQ 查询的方法:
选项 1:
public void MyMethod(List<MyObject> myList)
{
...
var isValid = myList.Where(l => l.IsActive)
.GroupBy(l => l.Category)
.Select(g => g.Count() > 300) //arbitrary number for the sake of argument
.Any();
}
选项 2:
public void MyMethod(List<MyObject> myList)
{
...
var isValid = myList.Where(l => l.IsActive)
.GroupBy(l => l.Category)
.Select(g => g.Count())
.Any(total => total > 300); //arbitrary number for the sake of argument
}
我想看看两者之间的性能是否有差异,所以我创建了一个控制台应用程序(如下所示)来比较它们。
发生的事情是首先执行的查询总是 运行s 慢,然后在随后的 运行s 它们都显示为 运行 在 0 毫秒内。然后我将比较值更改为 Ticks 并得到类似的结果。如果我切换查询的执行顺序,新的第一个现在 运行s 慢。
所以问题是双重的,为什么第一个执行的查询看起来比较慢?而且,有没有一种方法可以实际比较两者的性能?
测试代码如下:
static void Main(string[] args)
{
Console.WriteLine("Running test");
var rnd = new Random();
for (var i = 0;i < 5; i++)
{
RunTest(i, rnd);
Console.WriteLine();
Console.WriteLine();
}
Console.ReadKey();
}
private static void RunTest(int runId, Random rnd)
{
var list = GetData(rnd);
var startOne = DateTime.Now.TimeOfDay;
var one = list.Where(l => l.IsActive)
.GroupBy(l => l.Category)
.Select(g => g.Count() > 300)
.Any();
var endOne = DateTime.Now.TimeOfDay;
var startTwo = DateTime.Now.TimeOfDay;
var two = list.Where(l => l.IsActive)
.GroupBy(l => l.Category)
.Select(g => g.Count())
.Any(c => c > 300);
var endTwo = DateTime.Now.TimeOfDay;
var resultOne = (endOne - startOne).Milliseconds;
var resultTwo = (endTwo - startTwo).Milliseconds;
Console.WriteLine($"Results for test run #{++runId}");
Console.WriteLine();
Console.WriteLine($"Category 1 total: {list.Where(l => l.Category == 1 && l.IsActive).Count()}");
Console.WriteLine($"Category 2 total: {list.Where(l => l.Category == 2 && l.IsActive).Count()}");
Console.WriteLine($"Category 3 total: {list.Where(l => l.Category == 3 && l.IsActive).Count()}");
Console.WriteLine();
Console.WriteLine($"First option runs in: {resultOne} ");
Console.WriteLine();
Console.WriteLine($"Second option runs in: {resultTwo} ");
}
private static List<MyObject> GetData(Random rnd)
{
var result = new List<MyObject>();
for (var i = 0; i < 1000; i++)
{
result.Add(new MyObject { Category = rnd.Next(1, 4), IsActive = rnd.Next(0, 2) != 0 });
}
return result;
}
}
public class MyObject
{
public bool IsActive { get; set; }
public int Category { get; set; }
}
您的基准测试方法存在几个问题。
首先,当您有两个 DateTime
值并通过它们的 TimeOfDay
属性比较它们时...
var startOne = DateTime.Now.TimeOfDay;
// Do some work
var endOne = DateTime.Now.TimeOfDay;
var resultOne = (endOne - startOne).Milliseconds;
...然后你 运行 如果测试跨越一天 t运行 sition(午夜),你就有 负面 持续时间的风险.考虑一下...
DateTime midnight = DateTime.Today;
DateTime fiveSecondsBeforeMidnight = midnight - TimeSpan.FromSeconds(5);
DateTime fiveSecondsAfterMidnight = midnight + TimeSpan.FromSeconds(5);
Console.WriteLine($"Difference between DateTime values: {fiveSecondsAfterMidnight - fiveSecondsBeforeMidnight}");
Console.WriteLine($"Difference between TimeOfDay values: {fiveSecondsAfterMidnight.TimeOfDay - fiveSecondsBeforeMidnight.TimeOfDay}");
...打印...
Difference between DateTime values: 00:00:10
Difference between TimeOfDay values: -23:59:50
相反,您可以通过直接比较 DateTime
值来修复此错误并简化代码...
var startOne = DateTime.Now;
// Do some work
var endOne = DateTime.Now;
var resultOne = (endOne - startOne).Milliseconds;
这可以进一步改进,但是,通过使用 Stopwatch
class,它比比较 DateTime
值更准确,并且专门为此目的设计...
Stopwatch stopwatch = Stopwatch.StartNew();
// Do some work
TimeSpan resultOne = stopwatch.Elapsed;
stopwatch.Restart();
// Do some work
TimeSpan resultTwo = stopwatch.Elapsed;
二、TimeSpan.Milliseconds
property returns only the milliseconds component of the TimeSpan
value. To get the TimeSpan
value in milliseconds you want the TotalMilliseconds
property。考虑这里的区别...
TimeSpan value1 = TimeSpan.FromSeconds(1) + TimeSpan.FromMilliseconds(500);
TimeSpan value2 = TimeSpan.FromMilliseconds(900);
Console.WriteLine($" value1.Milliseconds: {value1.Milliseconds}");
Console.WriteLine($"value1.TotalMilliseconds: {value1.TotalMilliseconds}");
Console.WriteLine($" value2.Milliseconds: {value2.Milliseconds}");
Console.WriteLine($"value2.TotalMilliseconds: {value2.TotalMilliseconds}");
Console.WriteLine($"value1 is {(value1.Milliseconds < value2.Milliseconds ? "less" : "greater")} than value2 (by Milliseconds)");
Console.WriteLine($"value1 is {(value1.TotalMilliseconds < value2.TotalMilliseconds ? "less" : "greater")} than value2 (by TotalMilliseconds)");
...打印...
value1.Milliseconds: 500
value1.TotalMilliseconds: 1500
value2.Milliseconds: 900
value2.TotalMilliseconds: 900
value1 is less than value2 (by Milliseconds)
value1 is greater than value2 (by TotalMilliseconds)
像您一样比较 Ticks
属性 是解决此问题的另一种方法,或者您可以将时差存储为 TimeSpan
而无需选择其中一个属性并让字符串格式处理较小的组件...
TimeSpan resultOne = endOne - startOne;
TimeSpan resultTwo = endTwo - startTwo;
// ...
Console.WriteLine($"First option runs in: {resultOne:s\.ffffff} seconds");
Console.WriteLine();
Console.WriteLine($"Second option runs in: {resultTwo:s\.ffffff} seconds");
最后,我 运行 你的代码并看到与你所做的相同的结果:第一个 运行 是非零的,随后的 运行 是零。我的猜测是第一个 运行 需要更长的时间,因为您的代码尚未经过 JIT 优化。即使那些 "slow" 第一个 运行 也只需要几毫秒就可以完成,因为您的列表只有一千个项目。做空的基准 运行 无法提供有意义的比较。
进行上述更改并将 GetData()
返回的 List<>
的大小增加到 1000 万个项目后,每个 运行 需要几秒钟,第一个选项是几个在第一个 运行 中快了毫秒,在随后的 运行 中慢了 25-125 毫秒。
您可以考虑使用像 BenchmarkDotNet 这样的库,而不是滚动您自己的基准代码。它会处理一些细节,例如确定要执行多少 运行,"warming up" 您的代码以确保它已经过优化,以及为您计算统计数据。
是的,您可以使用 BenchmarkDotNet 准确比较两个选项的性能。这将成为一个简单的测试脚本来设置。
void Main()
{
var summary = BenchmarkRunner.Run<CollectionBenchmark>();
}
[MemoryDiagnoser]
public class CollectionBenchmark
{
private static Random random = new Random();
private List<MyObject> _list = new List<MyObject>();
[GlobalSetup]
public void GlobalSetup()
{
var rnd = new Random();
for (var i = 0; i < 1000; i++)
{
_list.Add(new MyObject { Category = rnd.Next(1, 4), IsActive = rnd.Next(0, 2) != 0 });
}
}
[Benchmark]
public void OptionOne()
{
var one = _list.Where(l => l.IsActive)
.GroupBy(l => l.Category)
.Select(g => g.Count() > 300)
.Any();
}
[Benchmark]
public void OptionTwo()
{
var two = _list.Where(l => l.IsActive)
.GroupBy(l => l.Category)
.Select(g => g.Count())
.Any(c => c > 300);
}
}
public class MyObject
{
public bool IsActive { get; set; }
public int Category { get; set; }
}
这在我的机器上产生了以下结果:
BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i5-6300U CPU 2.40GHz (Skylake), 1 CPU, 4 logical and 2 physical cores
Frequency=2437498 Hz, Resolution=410.2567 ns, Timer=TSC
[Host] : .NET Framework 4.6.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3324.0
DefaultJob : .NET Framework 4.6.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3324.0
| Method | Mean | Error | StdDev | Gen 0 | Allocated |
|---------- |---------:|----------:|----------:|-------:|----------:|
| OptionOne | 36.73 us | 0.7491 us | 1.9202 us | 8.4839 | 13.13 KB |
| OptionTwo | 36.37 us | 0.6993 us | 0.8053 us | 8.4839 | 13.13 KB |
分配的内存是一样的。考虑到基准测量的时间差为几分之一微秒,两者的性能没有实际差异。