Pull to refresh

Comments 40

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

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


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

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

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

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

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

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

Обещание сдержано не в полной мере
Очень большая разница в реализации 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 |
Microsoft на прошлой конференции представил утилиту для миграции, которая пока с трудом работает на демо-примерах, и требует кучу ручной работы на более-менее реальном проекте.
Официальная рекомендация — «не мигрировать старые приложения с Framework на Core». Читать как «мигрировать только тогда, когда Вы знаете зачем и без наших рекомендаций», то есть если и правда у Вас есть обработка больших данных и прочее, где эти проценты спасут ситуацию.
Здесь можно оценить, насколько foreach медленнее, чем for. Разница в их эффективности на Core составляет 3.5x (value types) и 12x (reference types), примерно также как и в полном фреймворке.

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

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

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

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

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

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

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

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

Проверил 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
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
Забавно, что просто пройти по листу 1000 стрингов в 2 раза медленнее, чем добавить 1000 стрингов.

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

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


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

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


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


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

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

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

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

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

У меня на 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). На гитхабе знают об этой проблеме, решений пока нет. Кто-нибудь сталкивался? В остальном эта платформа мне все больше и больше нравится.

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

Как это не подхватываются?
ну я в проекте 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 всё работало нормально. может я чтото не так делаю? буду благодарен если подскажете
Отличная статья. С одной стороны, и так очевидно, что последний .NET Core должен быть быстрее всех прошлых версий, в том числе при работе с коллекциями. Но теперь есть пруфы и цифры. Думаю, автор сэкономил многим интересующимся людям несколько вечеров.
С одной стороны, и так очевидно, что последний .NET Core должен быть быстрее всех прошлых версий, в том числе при работе с коллекциями.
Не вполне очевидно. Как вариант, реализация новых требований по безопасности может «замедлить» новую версию любого фреймворка.

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

Articles