Почему «ручной» foreach в три раза быстрее LINQ?

Изучаю LINQ, почему-то он работает несколько медленно.

Создаю словарь с рандомными данными:
public static class GenerateCollection
    {
        private static readonly Random random = new();

        public static Dictionary<string, int> Get(int workersNum)
        {
            Dictionary<string, int> stuff = new();

            while (stuff.Count < workersNum)
            {
                var workerName = Path.GetRandomFileName()[..4];
                var workerGrade = random.Next(1, 100);

                stuff[workerName] = workerGrade;
            }

            return stuff;
        }
    }


Беру два метода, один считает среднее при помощи LINQ, второй - "вручную", при помощи foreach:

public static class LinqMethods
    {
        public static double Average(Dictionary<string, int> stuff) => stuff.Average(stuff => stuff.Value);
    }

и
public static class ManualMethods
    {
        public static double Average(Dictionary<string, int> stuff)
        {
            long sum = 0;

            checked
            {
                foreach (var worker in stuff)
                {
                    sum += worker.Value;
                }
            }

            return (double) sum / stuff.Count;
        }
    }


Сравниваю при помощи BenchmarkDotNet:
public class LinqVsManual
    {
        private Dictionary<string, int> _stuffA = new();

        [Params(1_000, 10_000, 100_000)]
        public int WorkersNum { get; set; }

        [IterationSetup]
        public void Setup()
        {
            _stuffA = GenerateCollection.Get(WorkersNum);
        }

        [Benchmark]
        public double ManualAverage()
        {
            return ManualMethods.Average(_stuffA);
        }

        [Benchmark]
        public double LinqAverage()
        {
            return LinqMethods.Average(_stuffA);
        }
    }

    internal class Program
    {
        private static void Main()
        {
            BenchmarkRunner.Run<LinqVsManual>();
        }
    }


Получаю:
|        Method | WorkersNum |         Mean |
|-------------- |----------- |-------------:|
| ManualAverage |       1000 |     2.879 us |
|   LinqAverage |       1000 |    12.792 us | 
| ManualAverage |      10000 |    28.673 us | 
|   LinqAverage |      10000 |   115.260 us | 
| ManualAverage |     100000 |   391.715 us |
|   LinqAverage |     100000 | 1,158.016 us |


Думаю, это не LINQ настолько медленный, это я где-то ошибся, т. к. фактически повторяю код из github.com/dotnet/corefx/blob/master/src/System.Linq/src/System/Linq. Но где?
  • Вопрос задан
  • 356 просмотров
Решения вопроса 2
FoggyFinder
@FoggyFinder
Сейчас вы сравниваете версию с селектором (преобразующей функцией) с обычным циклом, вместо этого лучше сравнивать "чистую" версию:

public static double Average(Dictionary<string, int> stuff) => stuff.Values.Average();


тогда и разница будет не такой значительной.
Ответ написан
sarapinit
@sarapinit Куратор тега C#
Точу водой камень
А вы добавьте [MemoryDiagnoser] к вашему бенчмарку, сразу станет понятно.
Linq аллоцирует память за счет использования итераторов внутри.
А то что у вас в "ручном" методе, скорее всего, преобразуется компилятором в for с доступом по индексу и аллокаций там не будет.
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы