.NET Core vs Framework. Производительность коллекций

    image


    Релиз .NET Core 3.1 — хороший повод мигрировать свой проект с Framework на Core. Во-первых, это отполированная версия с долгосрочной поддержкой (LTS), т.е. её можно смело использовать в продакшене. Во-вторых, в третьей версии добавили поддержку WPF и WinForms, так что теперь появилась возможность мигрировать и десктопные приложения.


    Мне стало интересно, какой прирост производительности можно ожидать от Core в самых базовых классах, которые максимально часто используются в коде. Например, коллекции List, Array и Dictionary.


    Если вам тоже интересно, как и почему изменилась производительность основных коллекций в Core 3 — прошу под кат!


    Бенчмарки


    Для сравнительных тестов я взял три актуальных рантайма: .NET Framework 4.8, .NET Core 3.1 и .NET Core 2.1. Все замеры производились на следующей конфигурации:


    BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
    Intel Core i7-7700K CPU 4.20GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
    .NET Core SDK=3.1.101
    [Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
    Job-1  : .NET Framework 4.8 (4.8.4075.0), X64 RyuJIT
    Job-2  : .NET Core 2.1.15 (CoreCLR 4.6.28325.01, CoreFX 4.6.28327.02), X64 RyuJIT
    Job-3  : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT

    Также я прогонял все тесты на двух дополнительных машинах (на Haswell и Sky Lake), чтобы убедиться, что результаты тестов стабильны и воспроизводятся на другом железе.


    Класс ValuesGenerator (да и основу для самих бенчмарков) я позаимствовал из репозитория перфоманс-тестов. Эти тесты используются мейнтейнерами .NET Core для тестирования предлагаемых оптимизаций.


    List


    Цикл for


    Код List_IterateFor
    [GenericTypeArguments(typeof(int))]
    [GenericTypeArguments(typeof(string))]
    public class ListIterationFor<T>
    {
        [Params(100, 1_000, 10_000)]
        public int Size;
    
        private List<T> _list;
    
        [GlobalSetup]
        public void Setup() => _list = new List<T>(ValuesGenerator.ArrayOfUniqueValues<T>(Size));
    
        [Benchmark]
        public T List_IterateFor()
        {
            T result = default;
            List<T> collection = _list;
    
            for (int i = 0; i < collection.Count; i++)
                result = collection[i];
    
            return result;
        }
    }

    Method Runtime Size Mean Error StdDev Ratio
    IterateFor_Int .NET 4.8 1000 565.09 ns 0.191 ns 0.127 ns 1.00
    IterateFor_Int .NET Core 2.1 1000 451.14 ns 0.236 ns 0.156 ns 0.80
    IterateFor_Int .NET Core 3.1 1000 451.08 ns 0.143 ns 0.085 ns 0.80
    IterateFor_String .NET 4.8 1000 574.80 ns 6.795 ns 4.494 ns 1.00
    IterateFor_String .NET Core 2.1 1000 460.86 ns 3.771 ns 2.494 ns 0.80
    IterateFor_String .NET Core 3.1 1000 460.35 ns 0.681 ns 0.405 ns 0.80

    В Core JIT генерирует более эффективный код, чтение элементов из List в цикле for стало быстрее на ~20%.


    Цикл foreach


    Код List_IterateForEach
    [GenericTypeArguments(typeof(int))]
    [GenericTypeArguments(typeof(string))]
    public class ListIterationForEach<T>
    {
        [Params(100, 1_000, 10_000)]
        public int Size;
    
        private List<T> _list;
    
        [GlobalSetup]
        public void Setup() => _list = new List<T>(ValuesGenerator.ArrayOfUniqueValues<T>(Size));
    
        [Benchmark]
        public T List_IterateForEach()
        {
            T result = default;
            List<T> collection = _list;
    
            foreach (var item in collection)
                result = item;
    
            return result;
        }
    }

    Method Runtime Size Mean Error StdDev Ratio
    IterateForEach_Int .NET 4.8 1000 1,574.5 ns 2.73 ns 1.81 ns 1.00
    IterateForEach_Int .NET Core 2.1 1000 1,575.8 ns 3.82 ns 2.27 ns 1.00
    IterateForEach_Int .NET Core 3.1 1000 1,568.1 ns 0.61 ns 0.40 ns 1.00
    IterateForEach_String .NET 4.8 1000 8,046.3 ns 36.51 ns 24.15 ns 1.00
    IterateForEach_String .NET Core 2.1 1000 6,465.0 ns 15.26 ns 10.09 ns 0.80
    IterateForEach_String .NET Core 3.1 1000 5,886.3 ns 14.65 ns 9.69 ns 0.73

    Итерирование List с ссылочными типами через foreach стало быстрее на 27%, но для значимых типов ничего не поменялось. Здесь можно оценить, насколько foreach медленнее, чем for. Разница в их эффективности на Core составляет 3.5x (value types) и 12x (reference types), примерно также как и в полном фреймворке.


    Add


    Чтобы протестировать метод без ресайза внутреннего массива в тесте используется конструктор List с заданной ёмкостью (capacity).


    Код List_Add
    [GenericTypeArguments(typeof(int))]
    [GenericTypeArguments(typeof(string))]
    public class ListAdd<T>
    {
        private T[] _uniqueValues;
    
        [Params(100, 1_000, 10_000)]
        public int Size;
    
        [GlobalSetup]
        public void Setup() => _uniqueValues = ValuesGenerator.ArrayOfUniqueValues<T>(Size);
    
        [Benchmark]
        public List<T> List_Add()
        {
            List<T> collection = new List<T>(Size);
            T[] uniqueValues = _uniqueValues;
    
            for (int i = 0; i < uniqueValues.Length; i++)
                collection.Add(uniqueValues[i]);
    
            return collection;
        }
    }

    Method Runtime Size Mean Error StdDev Ratio
    Add_Int .NET 4.8 1000 2,006.5 ns 11.65 ns 6.93 ns 1.00
    Add_Int .NET Core 2.1 1000 1,249.0 ns 1.00 ns 0.60 ns 0.62
    Add_Int .NET Core 3.1 1000 1,260.9 ns 5.88 ns 3.89 ns 0.63
    Add_String .NET 4.8 1000 3,250.8 ns 53.13 ns 35.14 ns 1.00
    Add_String .NET Core 2.1 1000 2,816.8 ns 37.26 ns 22.18 ns 0.87
    Add_String .NET Core 3.1 1000 2,538.2 ns 30.55 ns 20.21 ns 0.78

    На Core 3 добавление работает быстрее на 22% (reference types) и 37% (value types). Что изменилось в коде метода? Добавление без ресайза, т.е. самый частый вариант выделен в отдельный метод с атрибутом [AggressiveInlining], т.е. он теперь инлайнится. Из мелких оптимизаций: убраны две лишние проверки выхода за границы и значение поля size теперь кешируется в локальную переменную.


    Contains


    Давайте возьмём негативный сценарий для метода Contains: будем искать элементы, которых нет в коллекции.


    Код List_Contains
    [GenericTypeArguments(typeof(int))]
    [GenericTypeArguments(typeof(string))]
    public class ListContains<T> where T : IEquatable<T>
    {
        [Params(100, 1_000, 10_000)]
        public int Size;
    
        private List<T> _list;
        private T[] _lookupValues;
    
        [GlobalSetup]
        public void Setup()
        {
            var uniqueValues = ValuesGenerator.ArrayOfUniqueValues<T>(Size * 2);
            _list = uniqueValues.Take(Size).ToList();
            _lookupValues = uniqueValues.Skip(Size).ToArray();
        }
    
        [Benchmark]
        public int List_Contains()
        {
            int count = 0;
            List<T> collection = _list;
            T[] array = _lookupValues;
    
            for (int i = 0; i < array.Length; i++)
            {
                if (collection.Contains(array[i]))
                    count++;
            }
    
            return count;
        }
    }

    Method Runtime Size Mean Error StdDev Ratio
    Contains_Int .NET 4.8 1000 1,128.975 us 5.4951 us 3.6347 us 1.00
    Contains_Int .NET Core 2.1 1000 456.040 us 0.1437 us 0.0950 us 0.40
    Contains_Int .NET Core 3.1 1000 188.002 us 0.1619 us 0.0964 us 0.17
    Contains_String .NET 4.8 1000 4,027.20 us 9.479 us 5.641 us 1.00
    Contains_String .NET Core 2.1 1000 3,332.93 us 2.156 us 1.128 us 0.83
    Contains_String .NET Core 3.1 1000 2,723.48 us 2.460 us 1.464 us 0.68

    На Core 3 поиск Int в List стал примерно в 6 раз быстрее, а поиск строк — в 1.4 раза. В Core JIT научился в некоторых ситуациях девирутализировать виртуальные методы, т.е. они вызываются напрямую. Более того, такие методы могут быть заинлайнены. В данном случае девиртуализируется метод EqualityComparer.Default.Equals, который используется для сравнения элементов. В случае с Int всё сводится к вызову Int32.Equals, который к тому же инлайнится. В итоге получившийся код по эффективности близок к прямому сравнению двух Int.


    Кстати, раньше я всегда думал, что метод Contains внутри вызывает IndexOf, но оказалось, что это верно только для Core. В полном фреймворке это разные методы, и работают они с разной скоростью.


    List Methods Summary


    Сводная таблица относительной производительности (ratio) основных методов List при N = 1000.


    List Method Type .NET 4.8 Core 2.1 Core 3.1 Details
    Ctor Int 1.00 0.82 0.47 Report
    Ctor String 1.00 0.90 0.92 Report
    IterateFor Int 1.00 0.80 0.80 Report
    IterateFor String 1.00 0.80 0.80 Report
    IterateForEach Int 1.00 1.00 1.00 Report
    IterateForEach String 1.00 0.80 0.73 Report
    Add Int 1.00 0.62 0.63 Report
    Add String 1.00 0.87 0.78 Report
    Contains Int 1.00 0.40 0.17 Report
    Contains String 1.00 0.83 0.68 Report
    IndexOf Int 1.00 0.99 0.43 Report
    IndexOf String 1.00 0.95 0.95 Report

    Array Methods Summary


    Подробно останавливаться на методах массива я не буду, поскольку List — это обертка над массивом.
    Так что здесь я приведу таблицу относительной производительности Array при N = 1000.


    Array Method Type .NET 4.8 Core 2.1 Core 3.1 Details
    Ctor Int 1.00 0.73 0.88 Report
    Ctor String 1.00 0.75 0.84 Report
    IterateFor Int 1.00 0.86 1.00 Report
    IterateFor String 1.00 1.00 1.00 Report
    IterateForEach Int 1.00 0.84 1.00 Report
    IterateForEach String 1.00 1.00 1.00 Report

    Здесь можно отметить, что как и прежде, цикл foreach для массива преобразуется в обычный for. Т.е. с точки зрения производительности для итерации массива нет разницы какой из циклов использовать.


    Dictionary


    Randomized Hash


    В .NET Core для расчета хешей строк теперь используется рандомизированный алгоритм (Marvin). Т.е. при каждом запуске приложения хеш одной и той же строки будет разным. Это защита от хеш-атак, в частности "hash flooding" (подробнее). Естественно, этот алгоритм медленнее, чем нерандомизированный. Чтобы производительность Dictionary со строковым ключом не просела, внутри него рандомизированный хеш включается только при достижении определённого количества коллизий (сейчас HashCollisionThreshold = 100).


    Add


    Код Dictionary_Add
    [GenericTypeArguments(typeof(int))]
    [GenericTypeArguments(typeof(string))]
    public class DictionaryAdd<T>
    {
        private T[] _uniqueValues;
    
        [Params(100, 1_000, 10_000)]
        public int Size;
    
        [GlobalSetup]
        public void Setup() => _uniqueValues = ValuesGenerator.ArrayOfUniqueValues<T>(Size);
    
        [Benchmark]
        public Dictionary<T, T> Dictionary_Add()
        {
            var collection = new Dictionary<T, T>(Size);
            var uniqueValues = _uniqueValues;
    
            for (int i = 0; i < uniqueValues.Length; i++)
                collection.Add(uniqueValues[i], uniqueValues[i]);
    
            return collection;
        }
    }

    Method Runtime Size Mean Error StdDev Ratio
    Add_IntKey .NET 4.8 1000 10.449 us 0.0690 us 0.0456 us 1.00
    Add_IntKey .NET Core 2.1 1000 12.270 us 0.0492 us 0.0325 us 1.17
    Add_IntKey .NET Core 3.1 1000 11.355 us 0.0723 us 0.0478 us 1.09
    Add_StringKey .NET 4.8 1000 33.229 us 0.0331 us 0.0219 us 1.00
    Add_StringKey .NET Core 2.1 1000 35.303 us 0.1821 us 0.1084 us 1.06
    Add_StringKey .NET Core 3.1 1000 26.976 us 0.1248 us 0.0825 us 0.81

    Добавление в Dictionary с ключом String стало быстрее на 19%. В случае с Int ключом результат (ratio) зависит от размера: на 100 — 0.95, на 1'000 — 1.09, на 10'000 — 0.93. Отклонения небольшие, возможно, это просто "шум". На других машинах отклонения ещё меньше. Будем считать, что с ключом типа Int добавление элемента происходит примерно с той же скоростью.


    GetValue


    Код Dictionary_GetValue
    [GenericTypeArguments(typeof(int))]
    [GenericTypeArguments(typeof(string))]
    public class DictionaryGetValue<T>
    {
        private Dictionary<T, T> _dictionary;
        private T[] _values;
    
        [Params(100, 1_000, 10_000)]
        public int Size;
    
        [GlobalSetup]
        public void Setup()
        {
            _values = ValuesGenerator.ArrayOfUniqueValues<T>(Size);
            _dictionary = _values.ToDictionary(i => i);
        }
    
        [Benchmark]
        public T Dictionary_GetValue()
        {
            Dictionary<T, T> collection = _dictionary;
            T[] values = _values;
    
            T result = default;
    
            for (int i = 0; i < values.Length; i++)
                result = collection[values[i]];
    
            return result;
        }
    }

    Method Runtime Size Mean Error StdDev Ratio
    GetValue_IntKey .NET 4.8 1000 10.916 us 0.019 us 0.013 us 1.00
    GetValue_IntKey .NET Core 2.1 1000 10.985 us 0.135 us 0.089 us 1.01
    GetValue_IntKey .NET Core 3.1 1000 9.424 us 0.086 us 0.056 us 0.86
    GetValue_StringKey .NET 4.8 1000 31.622 us 0.294 us 0.175 us 1.00
    GetValue_StringKey .NET Core 2.1 1000 31.787 us 0.090 us 0.047 us 1.00
    GetValue_StringKey .NET Core 3.1 1000 23.572 us 0.098 us 0.058 us 0.75

    Получение элемента по строковому ключу стало быстрее на 25%, по Int ключу — на 14%. Однако, здесь есть зависимость от размера Dictionary. Чем меньше размер — тем больше Framework отстает от Core 3 и наоборот. На маленьких размерах Core 3 работает в 1.5 раза быстрей. При достижении размера в 10'000 производительность Core 3 падает до уровня Framework и даже чуть ниже (см. отчеты ниже).


    В коде класса Dictionary слишком много изменений, чтобы однозначно сказать, какие из них больше всего повлияли на производительность.


    Dictionary Methods Summary


    Сводная таблица относительной производительности основных методов Dictionary при N = 1000.


    Dictionary Method Type .NET 4.8 Core 2.1 Core 3.1 Details
    Ctor Int 1.00 0.95 0.62 Report
    Ctor String 1.00 4.06 3.84 Report
    Add Int 1.00 1.17 1.09 Report
    Add String 1.00 1.06 0.81 Report
    GetValue Int 1.00 1.01 0.86 Report
    GetValue String 1.00 1.00 0.75 Report
    ContainsKey Int 1.00 0.84 0.78 Report
    ContainsKey String 1.00 0.99 0.73 Report
    ContainsValue Int 1.00 0.54 0.54 Report
    ContainsValue String 1.00 0.86 0.90 Report

    Результаты


    Как и ожидалось, почти все рассмотренные методы на Core 3 работают быстрее. Разница зачастую составляет 20-30%, а то и больше. Для таких базовых коллекций это отличный результат.


    Код и детальные результаты всех тестов доступны на GitHub.


    На сегодня Core практически догнал Framework по возможностям, а по производительности давно оставил его позади. Что касается ASP.NET Core — к третьей версии он вышел в топ самых производительных веб-фреймворков (топ-5 по последним тестам TechEmpower).


    Материалы по теме


    Стивен Тауб про оптимизации в .NET Core: Core 2.0, Core 2.1, Core 3.0
    Блоги: Андрей Акиньшин, Егор Богатов, Adam Sitnik, Matt Warren
    Материалы по .NET Performance
    Тесты веб-фреймворков от TechEmpower
    Репозиторий с перфоманс тестами
    Репозиторий рантайма Core
    Браузер исходного кода .NET Framework и .NET Core

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      +1
      Что касается ASP.NET Core — к третьей версии он вышел в топ самых производительных веб-фреймворков (топ-5 по последним тестам TechEmpower).

      Посмотрел ТОП, действительно пятое место, допустим у nginx на сях — 63, но… увидел надпись "kestrel" и сразу захотелось уточнить, что майкрософт не рекомендует выставлять kestrel в интернет. И сразу мне как-то показалось, что за такой высокий результат пришлось расплатиться именно порезанной безопасностью.


      Таки у меня вопрос: за прошедшие пару лет что-то поменялось в безопасности kestrel или что в 2.1, что в 3.1 разницы особой нет?

      +2
      На сегодня Core практически догнал Framework по возможностям, а по производительности давно оставил его позади. Что касается ASP.NET Core — к третьей версии он вышел в топ самых производительных веб-фреймворков (топ-5 по последним тестам TechEmpower).

      Полностью согласен. Еще с Core 2.0 начал изучение и честно сказать не был разочарован!
      И наконец-то появилась возможность мигрировать десктоп. Супер!
        –1
        Как там сейчас дела с GUI? вроде ни wpf ни windows forms не портированы.
          +3
          Портировали к релизу 3.0:
          github.com/dotnet/winforms
          github.com/dotnet/wpf

          Но пока что без дизайнера в студии, так что с миграцией я бы повременил. Чисто попробовать уже можно, да.
            +5
            • Windows-only. Для кроссплатформенных приложений только Avalonia ui
              +1
              С кроссплатформенными GUI всё сложно. Кроме Авалонии можно попробовать Xamarin.Forms, Eto.Forms, mono.Xwt, QtSharp.
              WPF и WinForms вряд ли когда-нибудь портируют на другие платформы.
                0

                Xamarin.Forms вроде только для мобильных приложений? Для остальных тулкитов нет дизайнера (кроме Qt, там вроде можно использовать QtDesigner и потом подгружать *.ui). Есть еще uno platform. Я вообще хочу Blazor client-side с готовыми компонентами (или дизайнер какой-нибудь, не знаю HTML и все связанные технологии), заворачивать в CefSharp (привет аля электрон приложения).

              +1

              Дизайнер для винформ в 16.5 в превью появился https://devblogs.microsoft.com/dotnet/wp-content/uploads/sites/10/2019/12/settings.png

                0
                О, я как-то пропустил этот момент, спасибо за наводку
                0
                Дизайнер для WPF уже есть в релизной версии.
            +4
            Если вам тоже интересно, как и почему изменилась производительность основных коллекций в Core 3 — прошу под кат!

            Обещание сдержано не в полной мере
              +2
              Очень большая разница в реализации Min/Max в SortedSet в .NET Core и .NET Framework. Т.к. под капотом SortedSet красно-чёрное бинарное дерево поиска, то для поиска max достаточно идти по правым предкам пока они не закончатся. Так и сделали в .NET Core, но в .NET Framework там страшный страх с созданием стэка и делегата для кастомной итерации.
              .NET Framework: referencesource.microsoft.com/#System/compmod/system/collections/generic/sortedset.cs,1677
              .NET Core: source.dot.net/#System.Collections/System/Collections/Generic/SortedSet.cs,1533
              Ну и бенчмарки, куда же без них? (markdown в комментариях не умеет в таблицы похоже)
              BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
              Intel Core i7-6700 CPU 3.40GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
              [Host]: .NET Framework 4.8 (4.8.4075.0), X64 RyuJIT
              DefaultJob: .NET Framework 4.8 (4.8.4075.0), X64 RyuJIT

              | Method | ElementsCount | Mean | Error | StdDev | Gen 0 | Allocated |
              |----------------- |-------------- |----------|---------|---------|-------|----------|
              | MaxFromSortedSet | 10 | 43.54 ns | 0.434 ns | 0.384 ns | 0.0516 | 217 B |
              | MaxFromSortedSet | 100 | 65.83 ns | 0.995 ns | 0.930 ns | 0.0631 | 265 B |
              | MaxFromSortedSet | 1000 | 85.91 ns | 1.723 ns | 1.612 ns | 0.0745 | 313 B |
              | MaxFromSortedSet | 10000 | 112.26 ns | 1.370 ns | 1.215 ns | 0.0899 | 377 B |
              | MaxFromSortedSet | 100000 | 134.96 ns | 1.135 ns | 1.007 ns | 0.1013 | 425 B |

              BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
              Intel Core i7-6700 CPU 3.40GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
              .NET Core SDK=3.1.101
              [Host]: .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
              DefaultJob: .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT

              | Method | ElementsCount | Mean | Error | StdDev |
              |----------------- |-------------- |----------:|----------:|----------:|
              | MaxFromSortedSet | 10 | 1.771 ns | 0.0676 ns | 0.0751 ns |
              | MaxFromSortedSet | 100 | 4.124 ns | 0.1060 ns | 0.0885 ns |
              | MaxFromSortedSet | 1000 | 6.175 ns | 0.1081 ns | 0.0903 ns |
              | MaxFromSortedSet | 10000 | 8.426 ns | 0.1551 ns | 0.1375 ns |
              | MaxFromSortedSet | 100000 | 11.245 ns | 0.1378 ns | 0.1151 ns |
                0
                Microsoft на прошлой конференции представил утилиту для миграции, которая пока с трудом работает на демо-примерах, и требует кучу ручной работы на более-менее реальном проекте.
                Официальная рекомендация — «не мигрировать старые приложения с Framework на Core». Читать как «мигрировать только тогда, когда Вы знаете зачем и без наших рекомендаций», то есть если и правда у Вас есть обработка больших данных и прочее, где эти проценты спасут ситуацию.
                  0
                  Здесь можно оценить, насколько foreach медленнее, чем for. Разница в их эффективности на Core составляет 3.5x (value types) и 12x (reference types), примерно также как и в полном фреймворке.

                  Интересно, почему такая большая разница? Я думал компилятор может оптимально развернуть foreach и для списков.

                    +2

                    Как минимум в foreach на каждой итерации есть проверка, не изменился ли список.

                      0
                      Интересно, в system.Immutable коллекциях та же шляпа?
                        +1

                        Исходный код открыт: Core
                        Выглядит как двойная шляпа.

                      0
                      В цикле foreach используется энумератор. Плюс, список во время итерации может измениться, так что всё завернуто в try-finally, примерно так:
                      List<T>.Enumerator enumerator = list.GetEnumerator();
                      try
                      {
                          while (enumerator.MoveNext())
                          {
                              T current = enumerator.Current;
                          }
                      }
                      finally
                      {
                          ((IDisposable)enumerator).Dispose();
                      }
                        0

                        Спасибо за разъяснение, возьму на вооружение в высокопроизводительном коде.

                          +2

                          Единственное исключение — массивы. Для них foreach эффективен.

                            +1

                            Да, об этом в статье написано.

                            0

                            Проверил assembler код для обхода списка с помощью foreach и for.с помощью sharplab.io. Мусора, конечно, немало:


                            ListForEach
                            C.ListForEach(System.Collections.Generic.List`1<Int32>)
                                L0000: sub rsp, 0x38
                                L0004: xor eax, eax
                                L0006: mov [rsp+0x20], rax
                                L000b: mov [rsp+0x28], rax
                                L0010: mov [rsp+0x30], rax
                                L0015: mov ecx, [rdx]
                                L0017: mov ecx, [rdx+0x14]
                                L001a: mov [rsp+0x20], rdx
                                L001f: xor eax, eax
                                L0021: mov [rsp+0x28], eax
                                L0025: mov [rsp+0x2c], ecx
                                L0029: mov [rsp+0x30], eax
                                L002d: lea rcx, [rsp+0x20]
                                L0032: call System.Collections.Generic.List`1+Enumerator[[System.Int32, System.Private.CoreLib]].MoveNext()
                                L0037: test eax, eax
                                L0039: jz L0052
                                L003b: mov ecx, [rsp+0x30]
                                L003f: call System.Console.WriteLine(Int32)
                                L0044: lea rcx, [rsp+0x20]
                                L0049: call System.Collections.Generic.List`1+Enumerator[[System.Int32, System.Private.CoreLib]].MoveNext()
                                L004e: test eax, eax
                                L0050: jnz L003b
                                L0052: add rsp, 0x38
                                L0056: ret

                            ListFor
                            C.ListFor(System.Collections.Generic.List`1<Int32>)
                                L0000: push rdi
                                L0001: push rsi
                                L0002: sub rsp, 0x28
                                L0006: mov rsi, rdx
                                L0009: xor edi, edi
                                L000b: cmp dword [rsi+0x10], 0x0
                                L000f: jle L0032
                                L0011: cmp edi, [rsi+0x10]
                                L0014: jae L0039
                                L0016: mov rcx, [rsi+0x8]
                                L001a: cmp edi, [rcx+0x8]
                                L001d: jae L003f
                                L001f: movsxd rax, edi
                                L0022: mov ecx, [rcx+rax*4+0x10]
                                L0026: call System.Console.WriteLine(Int32)
                                L002b: inc edi
                                L002d: cmp edi, [rsi+0x10]
                                L0030: jl L0011
                                L0032: add rsp, 0x28
                                L0036: pop rsi
                                L0037: pop rdi
                                L0038: ret
                                L0039: call System.ThrowHelper.ThrowArgumentOutOfRange_IndexException()
                                L003e: int3
                                L003f: call 0x7ffadf61ef00
                                L0044: int3

                            А вот для массива, без проверки на выход за границы:


                            ArrayFor
                            C.ArrayFor(Int32[])
                                L0000: push rdi
                                L0001: push rsi
                                L0002: push rbx
                                L0003: sub rsp, 0x20
                                L0007: mov rsi, rdx
                                L000a: xor edi, edi
                                L000c: mov ebx, [rsi+0x8]
                                L000f: test ebx, ebx
                                L0011: jle L0025
                                L0013: movsxd rcx, edi
                                L0016: mov ecx, [rsi+rcx*4+0x10]
                                L001a: call System.Console.WriteLine(Int32)
                                L001f: inc edi
                                L0021: cmp ebx, edi
                                L0023: jg L0013
                                L0025: add rsp, 0x20
                                L0029: pop rbx
                                L002a: pop rsi
                                L002b: pop rdi
                                L002c: ret
                              0
                              L0021: cmp ebx, edi
                              L0023: jg L0013

                              а это разве не проверка за выход за границы? Она на каждой итерации происходит.
                              На самом деле там всё зависит ещё и от того массив является локальной переменной или нет, статическим филдом или нет:
                              sharplab
                              Вот такой For будет самым быстрым:
                                  int For2() {
                                      int sum = 0;
                                      var tmpArray = array;
                                      for (int i = 0; i < tmpArray.Length; i++) {
                                          sum+=tmpArray[i];
                                      }
                                      return sum;
                                  }   
                              

                              А вообще я люблю побенчмаркать разное и разобраться почему так, собираю интересности в один гист, там и for/foreach для массива есть: gist.github.com/tdkkdt/420422f2eee1c15393d383ba7c8d1b9a
                            0
                            Забавно, что просто пройти по листу 1000 стрингов в 2 раза медленнее, чем добавить 1000 стрингов.
                              0

                              И как объясняется такая большая просадка между значимыми и ссылочными типами?

                                +3

                                Скорее всего, особенностью инстанцирования дженериков в .NET.
                                При использовании значимых типов для каждой комбинации дженерик-параметров в рантайме генерится отдельный код, который может быть оптимизирован, тогда как при использовании ссылочных типов используется универсальная реализация.


                                При использовании ссылочных типов в качестве параметров, используется общ

                              0

                              В библиотеках .NET Core вообще сделано много оптимизаций.


                              Например, я раньше часто встречал анти-паттерн в различных языках, когда максимальный элемент искался через сортировку и взятие первого элемента, и получалась сложность O(N log N) вместо O(N).


                              Так вот в .NET Core теперь так делать совершенно не зазорно. Дополнительная проверка в Linq приводит к тому, что комбинация .OrderBy(...).Max() не приводит к сортировке массива, а к желаемому результату.

                                0
                                Хмм, OrderBy ведь всегда был ленивый, и возвращал IEnumerable, за счёт чего же прирост? Неужели и вправду под капотом лежал массив который сортировали? Это же вразрез со всей идеологией linq и полный фейл.
                                  +2

                                  OrderBy при попытке перечисления вычитывал исходную последовательность во внутренний массив и сортировал его.
                                  В новой версии, если весь результат не нужен, то применяется частичная сортировка, у которой сложность N + K log K, где N — размер исходной последовательности, а K — запрашиваемый кусок отсортированной.
                                  ПС. .OrderBy(...).Max() все таки сортируем целиком. Оптимизация касается методов Take, First, Last

                                    0
                                    ПС. .OrderBy(...).Max() все таки сортируем целиком. Оптимизация касается методов Take, First, Last

                                    Верно. Я ошибся — имел в виду OrderBy(...).First() — короче, ситуация, когда нужно не просто максимальное значение, но ещё и элемент, при котором оно достигается.

                                0
                                У меня на 3.1 появились проблемы с миграциями, выдается ошибка:
                                Could not load file or assembly 'netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040). На гитхабе знают об этой проблеме, решений пока нет. Кто-нибудь сталкивался? В остальном эта платформа мне все больше и больше нравится.
                                  0

                                  и запускаться проекты на net.core тоже стали быстрее. там если открыть проект и сделать tiered compilation = true то и компилироваться вроде тоже быстрее будет.
                                  не понравилось то что базы данных из connection strings appsettings.json не подхватываются как это было раньше в .net framework проектах. и порадовало ещё net standard 2.1

                                    0
                                    Как это не подхватываются?
                                      0
                                      ну я в проекте core web app пишу в файле appsettings.json такие строки:

                                      «ConnectionStrings»: {
                                      «Habr1_Local»: «Server=(localdb)\\ProjectsV13;Database=Habr1_Local;Trusted_Connection=True;MultipleActiveResultSets=true»,

                                      потом открываю панельку Server Explorer, там Data Connections — и ничего, только те которые я глобально для других проектов добавлял. в .net framework всё работало нормально. может я чтото не так делаю? буду благодарен если подскажете
                                    +1
                                    Отличная статья. С одной стороны, и так очевидно, что последний .NET Core должен быть быстрее всех прошлых версий, в том числе при работе с коллекциями. Но теперь есть пруфы и цифры. Думаю, автор сэкономил многим интересующимся людям несколько вечеров.
                                      0
                                      С одной стороны, и так очевидно, что последний .NET Core должен быть быстрее всех прошлых версий, в том числе при работе с коллекциями.
                                      Не вполне очевидно. Как вариант, реализация новых требований по безопасности может «замедлить» новую версию любого фреймворка.

                                      Так что автору за эту статью полагается двойной респект.
                                      0
                                      А что там с производительностью throw? Было всё очень плохо в сравнении с обычным дотнет.
                                        0
                                        Как раз думал для следующей статьи взять что-нибудь вроде throw, lock и try-catch.
                                          +4
                                          Совершенно не хочу вас обидеть, но я посмотрел ваш профиль и у вас там 3 статьи:
                                          в 2012, 2016 и 2020 годах.
                                          Боюсь что с существуещим трендом вашу статью мы увидим не раньше 2024:)

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое