Из-за чего одинаковые делегаты работают по разному, В 10 Раз разница в скорости?
По вводным: что за ерунда наблюдается.
По вводным: в примере создаются 2 Одинаковых делегата(Ели ели нашел что в них дело, проверяя 1000 комбинаций причин), тот кто первый запустится будет в 10 раз быстрее. Почему так происходит, неожиданно поймал такое.
Сейчас ситуация такая: вот для простоты пример, замеряю работу функции, создал расширение For для выполнения N раз функции для элементов массивов, без него эффект не наблюдается, если допустим цикл внутрь m(()=>{for....}); вставить.
Сейчас ситуация такая: немного подебажил, и вроде заметил, что второй в порядке вызова метод вообще не инлайнится, и там прям стандартный метод вместо хотя бы 2 инструкций "lea lea eax, [rcx + rdx]; ret" там 10-20 как в debug режиме.
var arr=Enumerable.Range(0,1_000_000).Select(i=> (X:(uint)i, Y: 1u)).ToArray(); // от n не зависит, (кортеж артефакт поиска причины ) // одинаковые делегаты, второй в порядке вызывания будет до 10-100 раз медленнее!!!! var del = () => arr.For ((a) => test1(a.Item1, a.Item2)); var del2 = () => arr.For ((a) => test1(a.Item1, a.Item2)); m(del); // зависит от порядка вызова m(del2); // какая-то случ функция до 50 асемблерных строк всяких инструкций эффект наблюдается где-то static uint test1(uint i, uint j) { return i + j; } public static class ArrayExtensions{ public static void For <T>(this T[] arr, Action<T> func) { int len = arr.Length; foreach(var i in arr) { func(i); } } } public static void m(Action action, Action load = null) { Stopwatch sw = Stopwatch.StartNew(); load?.Invoke(); action(); load?.Invoke(); sw.Start(); action(); sw.Stop(); int n = (int)(5_000 / (sw.Elapsed.TotalMilliseconds + 0.001f)); if (n == 0) n = 1; List<double> times = new List<double>(); for (int i = 0; i < n; i++) { load?.Invoke(); sw.Restart(); action(); sw.Stop(); times.Add(sw.Elapsed.TotalMilliseconds); } double sum = times.Sum(); double average = sum / n; double err = Math.Sqrt(times.Aggregate(0.0, (acc, e) => acc += (e - average) * (e - average)) / (n - 1)); Console.WriteLine($"Test func: {action.Method.Name}"); Console.WriteLine($"Count invoke: {n}"); Console.WriteLine("Standard deviation: {0,20:f4} ms", err); Console.WriteLine("Max: {0,20:f4} ms", times.Max()); Console.WriteLine("Min: {0,20:f4} ms", times.Min()); var c1 = Console.ForegroundColor; Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("Mean time : {0,20:f4} ms", average); Console.ForegroundColor = c1; Console.WriteLine(); }
Test func: b__1
Count invoke: 5803
Standard deviation: 0.0146 ms
Max: 0.3772 ms
Min: 0.0362 ms
Mean time : 0.0509 msTest func: b__1
Count invoke: 55555
Standard deviation: 0.0091 ms
Max: 0.4715 ms
Min: 0.0355 ms
Mean time : 0.0420 msTest func: b__2
Count invoke: 3902
Standard deviation: 0.0589 ms
Max: 0.9081 ms
Min: 0.3880 ms
Mean : 0.4358 msTest func: b__2
Count invoke: 6124
Standard deviation: 0.0519 ms
Max: 0.7931 ms
Min: 0.3880 ms
Mean : 0.4263 ms
Опишите проблему, и специалист поможет с настройкой, исправлением ошибки или доработкой сайта. Подберём понятный план работ без лишней переписки.
Пока нет других ответов. Будьте первым, кто поможет автору.
Ответить на вопрос
Если два “одинаковых” делегата в C# дают разницу в скорости в 10 раз, первое подозрение — не сами делегаты, а прогрев JIT, tiered compilation, порядок запуска и методика замера. В .NET первый выполненный путь часто платит цену компиляции, оптимизации, кешей процессора и разогрева. Иногда наоборот: первый запускается в одном tier, второй уже получает оптимизированный код.
Для корректного сравнения не стоит мерить через самописный цикл с
Stopwatch, если речь о микропроизводительности. Используйте BenchmarkDotNet:[MemoryDiagnoser] public class DelegateBench { private readonly int[] data = Enumerable.Range(0, 1000).ToArray(); [Benchmark] public int First() { return data.Sum(x => x + 1); } [Benchmark] public int Second() { Func<int, int> f = x => x + 1; var sum = 0; foreach (var x in data) sum += f(x); return sum; } }
Запускать надо в Release без debugger:
dotnet run -c Releasedotnet run -c Release
Проверьте, не создаётся ли делегат внутри горячего цикла. Это уже не “одинаковая” операция: один вариант может переиспользовать делегат, другой — создавать новый объект или замыкание на каждой итерации.
Также обратите внимание на лямбды с захватом переменных. Лямбда без захвата может быть кеширована, а лямбда с захватом создаёт closure:
Func<int, int> a = x => x + 1; // без захвата int k = 1; Func<int, int> b = x => x + k; // с захватом
Итог: разница почти наверняка из-за JIT/tiered compilation, создания делегатов, замыканий или некорректного замера. Прогоните через BenchmarkDotNet, вынесите создание делегата из цикла и сравнивайте Release-сборку без отладчика.