Как стать автором
Обновить

Комментарии 16

Может для CRUD и формочек разницы нет, но в разработке фреймворков и алгоритмов Linq однозначно нет. К примеру, можете заоптимизировать такой код?

Некоторый код
	[Config(typeof(BenchmarkConfig))]
	public class Benchmark
	{
		readonly int[] Values = Enumerable.Range(0, 5000).Select(i => i).ToArray();
		const int OuterCount = 1000;

		[Benchmark]
		public int LinqBench()
		{
			var value = 0;
			
			for (var i = 0; i < OuterCount; i++)
				value |= Values.Where(j => j % 2 == 0).Sum();

			return value;
		}

		[Benchmark]
		public int ForBench()
		{
			var value = 0;

			for (var i = 0; i < OuterCount; i++)
			{
				var sum = 0;

				foreach (var val in Values)
					if (val % 2 == 0)
						sum += val;

				value |= sum;
			}

			return value;
		}
	}


Результат бенчмарка
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18362.836 (1903/May2019Update/19H1)
Intel Core i7-5820K CPU 3.30GHz (Broadwell), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.300
[Host]: .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
MediumRun: .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT

Job=MediumRun BuildConfiguration=Release Toolchain=.NET Core 3.1
IterationCount=15 LaunchCount=2 WarmupCount=10

| Method | Mean | Error | StdDev |
|---------- |----------:|----------:|----------:|
| LinqBench | 22.458 ms | 0.0325 ms | 0.0476 ms |
| ForBench | 3.865 ms | 0.0186 ms | 0.0273 ms |


но в разработке фреймворков и алгоритмов Linq однозначно нет

Про "алгоритмы" не знаю (хотя правильно примененный LINQ не обязательно меняет алгоритмическую сложность), а вот во фреймворках — да пожалуйста, сколько угодно. Главное, понимать, зачем.

Дело не в алгоритмической сложности, она же как раз не меняется, дело в лишних телодвижениях, которые тянет Linq (лишние вызовы методов, аллокации), в примере сверху разница на элементарной операции в ~6 раз. Фреймворки разные бывают, возможно в каких-нибудь вспомогательных функциях и используется, но точно не performance-critical (а в статье тег 'Высокая производительность'). Где-нибудь в ядре asp.net core или roslyn Linq врядли используется хоть в одном методе, который десятки тысяч раз в секунду выполняется. Понимать зачем, конечно, надо.

Я наблюдаю некоторую разницу между утверждениями "в разработке фреймворков [...] Linq однозначно нет" и "Фреймворки разные бывают [...] но точно не performance-critical"


Где-нибудь в ядре asp.net core или roslyn Linq врядли используется хоть в одном методе, который десятки тысяч раз в секунду выполняется.

И много таких методов в среднестатистическом фреймворке? А так-то в asp.net Core больше тысячи упоминаний System.Linq (да, я вижу, что многие из них — в тестах и билдере).

Согласен, разница в утверждениях есть, я потому мысль свою уточнил. Упоминания System.Linq в roslyn и asp.net core я тоже смотрел, там где Linq не влияет на производительность он используется, что и логично. Но вы же не станете спорить, что Linq добавляет некоторый overhead, и если в ядре какого-нибудь фреймворка метод выполняться будет сотни миллионов раз, и написание + пары строк для простого цикла сэкономит хотя бы 1% производительности, то можно пожертвовать выразительностью?
Но вы же не станете спорить, что Linq добавляет некоторый overhead

Не буду. Абстракции вообще обычно добавляют оверхед.


если в ядре какого-нибудь фреймворка метод выполняться будет сотни миллионов раз, и написание + пары строк для простого цикла сэкономит хотя бы 1% производительности, то можно пожертвовать выразительностью?

Если. Сколько раз типичный программист сталкивается с подобными методами за свою карьеру?

Я не знаю про типичных программистов, может они и фреймворки, типа asp.net core, не разрабатывают. В любом случае, нужно просто правильно выбирать инструмент для решения задачи, а не топить за Linq добро/зло и разоблачающие статьи на эту тему писать.
В любом случае, нужно просто правильно выбирать инструмент для решения задачи, а не топить за Linq добро/зло и разоблачающие статьи на эту тему писать.

Ну так чтобы правильно выбирать инструмент, нужно понимать, где он добро. А не писать вещи типа "в разработке фреймворков [...] однозначно нет".

Вы придрались к одной фразе, которую я уже пояснил и согласился, что она слишком категорична. Как бы больше нет причины продолжать спор по этому поводу.
Но это-же нечестно, оптимизатор просто выкидывает вложенный цикл из второго бенчмарка, давайте хотя-бы так, уже не так однозначно получается:
Заголовок спойлера
		[Benchmark]
		public int LinqBench()
		{
			var value = 0;

			for (var i = 1; i < OuterCount; i++)
			{
				value |= Values.Where(j => j % i == 0).Sum();
			}

			return value;
		}

		[Benchmark]
		public int ForBench()
		{
			var value = 0;

			for (var i = 1; i < OuterCount; i++)
			{
				var sum = 0;

				foreach (var val in Values)
					if (val % i == 0)
						sum += val;

				value |= sum;
			}

			return value;
		}



BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.836 (1909/November2018Update/19H2)
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.202
[Host]: .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
DefaultJob: .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT

| Method | Mean | Error | StdDev |
|---------- |---------:|---------:|---------:|
| LinqBench | 17.81 ms | 0.350 ms | 0.535 ms |
| ForBench | 13.34 ms | 0.261 ms | 0.268 ms |
Я, если честно, не смотрел asm код, но на каком основании оптимизатор должен был выкинуть вложенный цикл? Он эвристически в момент компиляции вычислил, что на каждой итерации внешнего цикла результат вложенного будет одинаковым и битовое 'или' также даст одинаковый результат? Он так не имеет права сделать, может я в другом потоке этот массив параллельно изменяю. В вашем примере просто операция вычисления остатка от деления на произвольное число сильнее нагрузило процессор, чем остаток от деления на 2, потому и накладные расходы Linq на этом фоне кажутся меньше. Замените в своём примере 'j % i == 0' на '(j+i) % 2 == 0', если так хотите, чтобы i участвовала. На моём компьютере 26 мс против 4мс. В любом случае, вы уже изменили алгоритм, а не оптимизировали, мне нужен был именно результат 1-го варианта, а не другой алгоритм.
Да, действительно, был не прав, (j+i) % 2 опять показывает большой отрыв.
Первый вариант слишком легко оптимизировать, достаточно сделать два последовательных цикла вместо вложенных, но это уже никакого отношения к linq vs обычный цикл не имеет.
На самом деле в данном случае цикл выигрывает у linq варианта потому, что обращается к элементам массива по индексу, что значительно быстрее, чем итератор. Если тип у Values будет какой-нибудь IReadOnlyCollection, результаты сравняются и linq даже может чуточку выигрывать.
Первый вариант на моей машине, поменял тип на IReadOnlyCollection:

| Method | Mean | Error | StdDev |
|---------- |---------:|---------:|---------:|
| LinqBench | 23.65 ms | 0.465 ms | 0.886 ms |
| ForBench | 25.52 ms | 0.474 ms | 0.633 ms |

Так что если к вам данные приходят в виде IEnumerable, IReadOnlyCollection или еще в чем-то, где нельзя обратиться по индексу, то отказываться от linq нет особого смысла.
Я не призываю отказываться от Linq, мне он нравится и я им пользуюсь. Но нужно понимать, что он тоже имеет свою цену.
Оффтоп:
Посему, я часто пишу
Первый абзац
Спасибо, интересная статься!
Сколько не писал, в 99% случаев оптимизировать приходится процедуры и функции БД, на C# как бы ты оптимально код не написал, как правило ничего не меняет, за исключением редких случаев, если в БД бардак — а чаще именно там основные проблемы.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории