Производительность LINQ: Отличие от LINQ в C # 6? - PullRequest
0 голосов
/ 25 апреля 2018

Редактировать: По предложению DavidG я увеличил количество предметов в 100 раз.Я перезапустил сравнение в режиме релиза и обновил результаты ниже.Я также обновил код на тот случай, если кто-то просто скопирует, вставит и запустит его локально.


Так что я видел много публикаций на SO о LINQ против производительности for -большинству из которых уже несколько лет - и я хотел увидеть это в действии для себя.Поэтому я написал небольшое приложение для тестирования, и результаты оказались ... не совсем такими, как я ожидал. У меня вопрос: изменения и оптимизации в C # 6 сделали всю проблему производительности неактуальной?

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

Я знаю, что все еще есть хорошие вопросы об использовании ручных циклов и LINQ изс точки зрения использования памяти, но либо мое приложение для сравнения имеет серьезные недостатки, либо кажется, что разница в скорости больше невелика.Возможно, он был оптимизирован в более поздних версиях C #?

Мое приложение ниже.Он придуман, я признаю - это наихудший сценарий, когда мы пытаемся найти предмет, который будет найден мертвым последним в списке из миллиона предметов, - но я взял его, основываясь на других сообщениях здесь.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace LinqDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            new Program().Run();
        }

        public void Run()
        {
            RunPerformanceComparison();
            Console.ReadKey();
        }

        private void RunPerformanceComparison()
        {
            Func<string, bool> criteriaFunction = d => d.Equals("YES");

            var data = new string[100000000];
            for (int i = 0; i < data.Length - 1; i++)
            {
                data[i] = "NO";
            }

            data[data.Length - 1] = "YES";


            Console.WriteLine("With LINQ");
            Console.WriteLine("------------");
            DoPerformanceRunLinq(data, criteriaFunction);

            Console.WriteLine();

            Console.WriteLine("Without LINQ");
            Console.WriteLine("------------");
            DoPerformanceRunManual(data, criteriaFunction);
        }

        private void DoPerformanceRunLinq(string[] data, Func<string, bool> criteriaFunction)
        {
            Stopwatch sw = new Stopwatch();
            for (int i = 0; i < 10; i++)
            {
                sw.Start();
                var result = data.Where(criteriaFunction).Select(d => d).ToList();
                sw.Stop();
                Console.WriteLine($"Iteration {i + 1}\tElapsed: {sw.Elapsed.TotalMilliseconds.ToString("n2")} ms");
                sw.Reset();
            }
        }

        private void DoPerformanceRunManual(string[] data, Func<string, bool> criteriaFunction)
        {
            Stopwatch sw = new Stopwatch();
            for (int i = 0; i < 10; i++)
            {
                sw.Start();
                var result = GetItems(data, criteriaFunction);
                sw.Stop();
                Console.WriteLine($"Iteration {i + 1}\tElapsed: {sw.Elapsed.TotalMilliseconds.ToString("n2")} ms");
                sw.Reset();
            }
        }

        private IEnumerable<string> GetItems(string[] data, Func<string, bool> criteriaFunction)
        {
            var ret = new List<string>();

            // Not deferred; runs all at once
            for (int i = 0; i < data.Length; i++)
            {
                if (criteriaFunction(data[i]))
                {
                    ret.Add(data[i]);
                }
            }

            return ret;
        }
    }
}

Вот результат выполнения этого (я запустил его в командной строке без VS):

With LINQ
------------
Iteration 1     Elapsed: 602.39 ms
Iteration 2     Elapsed: 522.72 ms
Iteration 3     Elapsed: 601.15 ms
Iteration 4     Elapsed: 518.71 ms
Iteration 5     Elapsed: 511.38 ms
Iteration 6     Elapsed: 565.92 ms
Iteration 7     Elapsed: 506.51 ms
Iteration 8     Elapsed: 524.91 ms
Iteration 9     Elapsed: 540.85 ms
Iteration 10    Elapsed: 502.33 ms

Without LINQ
------------
Iteration 1     Elapsed: 496.09 ms
Iteration 2     Elapsed: 496.15 ms
Iteration 3     Elapsed: 540.53 ms
Iteration 4     Elapsed: 549.28 ms
Iteration 5     Elapsed: 404.46 ms
Iteration 6     Elapsed: 407.23 ms
Iteration 7     Elapsed: 461.39 ms
Iteration 8     Elapsed: 414.90 ms
Iteration 9     Elapsed: 405.67 ms
Iteration 10    Elapsed: 437.98 ms

Более 100 миллионов строк, это лучшая производительность на стороне for, но не значительными суммами, которые некоторые люди требовали в прошлом (я слышал, как разница в 10 раз. Это даже не близко).Плюс, это 100 миллионов строк в памяти - я не думаю, что оптимизация здесь будет сделана путем выбора ручных циклов против LINQ.:) На самом деле, я не уверен, что разница здесь достаточно велика для того, чтобы кого-либо действительно волновало, если только вам абсолютно не нужна каждая последняя микросекунда производительности.В общем, я бы назвал это стиркой.

Я где-то испортил свое приложение, это просто неверное сравнение, или что-то изменилось внутри .NET?

1 Ответ

0 голосов
/ 25 апреля 2018

Теперь вы измеряете Where производительность в наилучших возможных для нее условиях.Он оптимизирован (имеет специальную обработку) для некоторых распространенных случаев.Вы используете его с массивом, а с массивами Where будет повторять их практически с тем же кодом, который вы использовали в «ручном» сценарии.Поскольку совпадение только одно, метод итератора MoveNext будет вызываться только один раз (ну, может быть, дважды).Короче говоря - Where и ручной цикл в ваших условиях имеет схожую производительность, потому что они запускают похожий код.

Если вы хотите наблюдать (искусственно) плохую производительность без особых изменений, попробуйте следующее:

data.Select(c => c).Where(criteriaFunction).ToList();

Теперь то, что вы передаете Where, является не массивом, а "реальным" IEnumerable (возвращаемым Select), и специальная обработка для массивов не применяется.Я запустил ваш код с этой модификацией и Where затем работает в 4 раза медленнее, чем ручной цикл.

Если это кажется несправедливым и далеким от реального использования, вы можете сделать это следующим образом:

class DataItem {
    public string Value { get; set; }
}

// loop version
private IEnumerable<string> GetItems(DataItem[] data, Func<string, bool> criteriaFunction) {
    var ret = new List<string>();
    // Not deferred; runs all at once
    for (int i = 0; i < data.Length; i++) {
        if (criteriaFunction(data[i].Value)) {
            ret.Add(data[i].Value);
        }
    }

    return ret;
}

// linq version
var result = data.Select(c => c.Value).Where(criteriaFunction).ToList();

Это то, что действительно можно сделать с помощью LINQ, и эта версия цикла с вашими данными примерно в 4 раза медленнее.Конечно, версию LINQ можно оптимизировать, но дело в том, что в некоторых случаях LINQ может быть значительно медленнее, особенно если вы неосторожны.

Таких примеров много.Считайте, что это абсолютно невинное LINQ Count:

var result = data.Count(c => c.Value == "YES");

И аналог с for:

private int ForCount(DataItem[] data) {
    int res = 0;
    for (int i = 0; i < data.Length; i++) {                
        if (data[i].Value == "YES") {
            res++;
        }
    }
    return res;
}

LINQ в 3 раза медленнее.И так далее.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...