Первый из двух эквивалентных запросов LINQ всегда выполняется медленнее - PullRequest
0 голосов
/ 09 марта 2019

Рассмотрим следующие два способа написания этого запроса 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
}

Я хотел посмотреть, есть ли разница в производительности между ними, поэтому я создал консольное приложение (показано ниже), чтобы сравнить их.

То, что происходит, заключается в том, что сначала выполняется запрос, который всегда выполняется медленнее, а затем при последующих запусках они оба отображаются как выполненные за 0 миллисекунд. Затем я изменил значение сравнения на Ticks и получил аналогичные результаты. Если я переключу порядок, в котором выполняются запросы, новый первый теперь будет работать медленнее.

Итак, вопрос двоякий: почему первый выполненный запрос кажется более медленным? И есть ли способ, которым я могу сравнить производительность двух?

Вот код тестирования:

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; }
    }

Ответы [ 2 ]

4 голосов
/ 09 марта 2019

Да, вы можете точно сравнить производительность двух ваших вариантов, используя 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 |

Память выделена одинаково. Учитывая, что эталонный тест измеряет разницу во времени в долях микросекунды, практической разницы в производительности также нет.

2 голосов
/ 09 марта 2019

Есть несколько проблем с вашей методологией бенчмаркинга.

Во-первых, когда у вас есть два значения DateTime, и вы сравниваете их по их TimeOfDay свойствам ...

var startOne = DateTime.Now.TimeOfDay;
// Do some work
var endOne = DateTime.Now.TimeOfDay;
var resultOne = (endOne - startOne).Milliseconds;

... тогда вы рискуете получить отрицательную длительность, если тест охватит дневной переход (полночь).Учтите это ...

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 класса , который является более точным, чем сравнение значений DateTime и специально разработанных для этой цели....

Stopwatch stopwatch = Stopwatch.StartNew();
// Do some work
TimeSpan resultOne = stopwatch.Elapsed;

stopwatch.Restart();
// Do some work
TimeSpan resultTwo = stopwatch.Elapsed;

Во-вторых, свойство TimeSpan.Milliseconds возвращает только миллисекунды компонент значения TimeSpan.Чтобы получить значение TimeSpan в миллисекундах , необходимо свойство TotalMilliseconds .Рассмотрим разницу здесь ...

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.Даже эти «медленные» первые запуски занимают всего несколько миллисекунд, потому что ваш список состоит всего из тысячи элементов.Короткие тесты не дают значимых сравнений.

После внесения изменений, описанных выше, и увеличения размера List<>, возвращаемого GetData() до 10 миллионов элементов, каждый цикл занимает несколько секунд, при этом первыйОпция может быть на несколько миллисекунд быстрее при первом запуске и на 25-125 миллисекунд медленнее при последующих запусках.

Вместо того, чтобы переходить к собственному тестовому коду, вы можете использовать библиотеку, такую ​​как BenchmarkDotNet .Он обрабатывает такие детали, как определение количества выполненных прогонов, «прогрев» вашего кода, чтобы убедиться, что он уже оптимизирован, и вычисление статистики для вас.

...