Cравнение string и Span<T>
Решил обновить старую статью о сравнении string и Span<T>, так как в ней были допущены ошибки в бенчмарках. Ссылку прикладывать не буду по этой же причине. На этот раз планирую провести замеры во всех методах, которые есть и в string, и в MemoryExtensions, и подготовить что-то вроде cheetsheet. Это будет таблица, к которой можно обратиться, чтобы понять, а стоит ли заморачиваться со Span<T> в конкретном случае или же производительность будет одинаковой.
Но перед публикацией хочу понять, насколько аудитория знакома со Span<T> и его преимуществами. Если выяснится, что большинство плохо знакомы с этой структурой, я опубликую список источников, где можно восполнить пробелы в знаниях.
Решил обновить старую статью о сравнении string и Span<T>, так как в ней были допущены ошибки в бенчмарках. Ссылку прикладывать не буду по этой же причине. На этот раз планирую провести замеры во всех методах, которые есть и в string, и в MemoryExtensions, и подготовить что-то вроде cheetsheet. Это будет таблица, к которой можно обратиться, чтобы понять, а стоит ли заморачиваться со Span<T> в конкретном случае или же производительность будет одинаковой.
Но перед публикацией хочу понять, насколько аудитория знакома со Span<T> и его преимуществами. Если выяснится, что большинство плохо знакомы с этой структурой, я опубликую список источников, где можно восполнить пробелы в знаниях.
👍11
Ваш опыт использования Span<T> в C#?
Final Results
14%
Что это такое? 😳
37%
Слышал(а), но не использовал(а)
28%
Использовал(а) несколько раз
21%
Использую регулярно
3 полезных ресурса про Span<T>
По итогам опроса, 51% не использовали Span в работе. Попробую помочь это исправить – начните со следующих ресурсов:
📺 Adam SITNIK: Spanification @ Update Conference Prague 2018.
Доклад Адама Ситника, полностью посвящённый Span<T>. Адам Ситник — разработчик в Microsoft, отвечающий за производительность .NET и один из основных мейнтейнеров библиотеки BenchmarkDotNet.
📺 High-performance code design patterns in C#. Konrad Kokosa .NET Fest 2019.
Доклад Конрада Кокосы о паттернах для высокой производительности. На 16:15 он рассказывает про Span<T>.
📕 Pro .NET Memory Management.
Книга Конрада Кокосы об управлении памятью в .NET. Есть на русском, но перевод мне совсем не нравится. Рекомендую читать оригинал. Глава 14 начинается с рассказа про Span<T>. Впрочем, лучше прочесть всю книгу целиком.
На следующей неделе опубликую первую часть результатов сравнения string и Span.
По итогам опроса, 51% не использовали Span в работе. Попробую помочь это исправить – начните со следующих ресурсов:
Доклад Адама Ситника, полностью посвящённый Span<T>. Адам Ситник — разработчик в Microsoft, отвечающий за производительность .NET и один из основных мейнтейнеров библиотеки BenchmarkDotNet.
Доклад Конрада Кокосы о паттернах для высокой производительности. На 16:15 он рассказывает про Span<T>.
📕 Pro .NET Memory Management.
Книга Конрада Кокосы об управлении памятью в .NET. Есть на русском, но перевод мне совсем не нравится. Рекомендую читать оригинал. Глава 14 начинается с рассказа про Span<T>. Впрочем, лучше прочесть всю книгу целиком.
На следующей неделе опубликую первую часть результатов сравнения string и Span.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
Сравнение string и Span: поиск символов и подстрок
Публикую первую часть результатов сравнения методов string и Span. Начнём с методов для поиска символов и подстрок. Замеры проводились на массиве из 100 000 строк. Я постарался скомпоновать данные в виде диаграмм, но поскольку я сравнивал перегрузки с различными значениями StringComparison, то данных получилось очень много. Сырые результаты и бенчмарки можно найти здесь. Основные выводы, которые можно сделать на основе полученных данных ниже.
Contains
🟢 Методы string.Contains(char value), string.Contains(string value), ReadOnlySpan<char>(char value) работают одинаково быстро.
🟢 Методы Contains(char value, StringComparison comparisonType) и Contains(string value, StringComparison comparisonType) работают быстро при использовании Ordinal, OrdinalIgnoreCase, InvariantCulture и InvariantCultureIgnoreCase.
🟠 Использование CurrentCulture и CurrentCultureIgnoreCase медленее в 250 – 330 раз.
StartsWith
🟢 Методы StartsWith(string value, StringComparison comparisonType) работают быстро при использовании Ordinal, OrdinalIgnoreCase, InvariantCulture и InvariantCultureIgnoreCase.
🟠 Использование CurrentCulture и CurrentCultureIgnoreCase медленее примерно в 100 раз.
🟠 Метод string.StartsWith(string value) под капотом использует перегрузку с параметром CurrentCulture.
EndsWith
🟢 Методы EndsWith(string value, StringComparison comparisonType) работают быстро при использовании Ordinal, OrdinalIgnoreCase, InvariantCulture и InvariantCultureIgnoreCase.
🟠 Использование CurrentCulture и CurrentCultureIgnoreCase медленее примерно в 100 раз.
🟠 Метод string.EndsWith(string value) под капотом использует перегрузку с параметром CurrentCulture.
IndexOf
🟢 Метод IndexOf(char value) работает одинаково быстро как в string, так и в Span.
🟢 Метод IndexOf(string value) работает быстро только в ReadOnlySpan.
🟢 Методы IndexOf(char value, StringComparison comparisonType) и IndexOf(string value, StringComparison comparisonType) работают быстро при использовании Ordinal, OrdinalIgnoreCase, InvariantCulture и InvariantCultureIgnoreCase.
🟠 Использование CurrentCulture и CurrentCultureIgnoreCase медленее примерно в 260 раз.
🟠 Метод string.IndexOf(string value) под капотом использует перегрузку с параметром CurrentCulture.
LastIndexOf
🟢 Метод LastIndexOf(char value) работает одинаково быстро как в string, так и в Span.
🟢 Методы LastIndexOf(char value, StringComparison comparisonType) и LastIndexOf(string value, StringComparison comparisonType) работают быстро при использовании Ordinal, OrdinalIgnoreCase, InvariantCulture и InvariantCultureIgnoreCase.
🟠 Использование CurrentCulture и CurrentCultureIgnoreCase медленее примерно в 260-370 раз.
🟠 string.LastIndexOf(string value) под капотом этот метод использует перегрузку с параметром CurrentCulture.
IndexOfAny и LastIndexOfAny
🟢 Все методы работают одинаково быстро.
Публикую первую часть результатов сравнения методов string и Span. Начнём с методов для поиска символов и подстрок. Замеры проводились на массиве из 100 000 строк. Я постарался скомпоновать данные в виде диаграмм, но поскольку я сравнивал перегрузки с различными значениями StringComparison, то данных получилось очень много. Сырые результаты и бенчмарки можно найти здесь. Основные выводы, которые можно сделать на основе полученных данных ниже.
Contains
StartsWith
EndsWith
IndexOf
LastIndexOf
IndexOfAny и LastIndexOfAny
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12
FrozenDictionary Under the Hood: How Fast Is It Compared to Dictionary and Why
I’m happy to share the translation of my article about a new type of generic collections in C#: FrozenDictionary. The main feature of this dictionary is that it’s immutable, but allows reading the data faster comparing to a standard Dictionary.
I split the results on the cover by a reason: the algorithms used in FrozenDictionary are highly depended on key type, the size of the array or even the number of the string keys with the same length.
Curious to learn more? Check out the article!
I’m happy to share the translation of my article about a new type of generic collections in C#: FrozenDictionary. The main feature of this dictionary is that it’s immutable, but allows reading the data faster comparing to a standard Dictionary.
I split the results on the cover by a reason: the algorithms used in FrozenDictionary are highly depended on key type, the size of the array or even the number of the string keys with the same length.
Curious to learn more? Check out the article!
alexeyfv
FrozenDictionary under the hood: how fast is it comparing to Dictionary and why
With .NET 8 release, C# developers received a new type of generic collections – FrozenDictionary. The main feature of this dictionary is that it’s immutable, but allows reading the data faster comparing to a plain Dictionary. I split the results on the cover…
👍6
English version is below.
Продолжаю сравнивать методы string и ReadOnlySpan<char>.
По результатам бенчмарка, методы Trim, TrimStart, TrimEnd в ReadOnlySpan<char> в среднем на 40% быстрее, чем в string. Ничего удивительного нет, т.к. использование этих же методов в string приводит к аллокациям памяти.
Метод string.CompareTo во всех случаях быстрее, правда незначительно. При использовании StringComparison.Ordinal эта разница достигает 88%.
Метод Equals в ReadOnlySpan<char> демонстрирует сопоставимую производительность с string при использовании CurrentCulture, InvariantCulture и других типах сравнения. Однако при использовании OrdinalIgnoreCase string быстрее ~25%.
String and Span comparison. Part 2. Trim, TrimStart, TrimEnd, CompareTo, Equals
I continue to compare the string and ReadOnlySpan<char> methods.
According to the benchmark results, Trim, TrimStart, and TrimEnd methods in ReadOnlySpan<char> are, on average, 40% faster than in string. This is not surprising, because using these methods in string causes memory allocations.
CompareTo is faster in string in all cases, although only slightly. But when using StringComparison.Ordinal, the difference reaches 88%.
Equals in ReadOnlySpan<char> shows comparable performance to string when using all comparison types except OrdinalIgnoreCase. In this case string is ~25% faster.
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
English version is below.
Продолжаю сравнивать методы string и ReadOnlySpan<char>.
Метод Split работает на 46% – 64% быстрее в ReadOnlySpan<char>. Использование метода MemoryExtensions.Split позволяет работать со структурой Range, массив которых можно создать в стеке:
Span<Range> ranges = stackalloc Range[str.Length];
str.AsSpan().Split(ranges, separator);
Методы ToLower и ToUpper также работают на 43% – 45% быстрее в ReadOnlySpan<char>. И опять же, это преимущество достигается за счёт отсутствия аллокаций в куче:
Span<char> destination = stackalloc char[str.Length];
str.AsSpan().ToLower(destination, CultureInfo.InvariantCulture);
Производительность метода CopyTo практически одинаковая — разница составляет всего 3% в пользу ReadOnlySpan. Однако важно учитывать, что массив, в который будет скопирована строка, должен быть заранее проинициализирован. Это можно сделать как в стеке с помощью stackalloc, так и в куче с помощью оператора new, что может влиять на производительность:
// Вариант 1
Span<char> destination = new char[str.Length];
str.CopyTo(destination);
// Вариант 2
Span<char> destination = stackalloc char[str.Length];
str.CopyTo(destination);
Метод Replace в ReadOnlySpan<char> работает на 75% быстрее, чем в string. Это очевидно связано с отсутствием аллокаций при использовании ReadOnlySpan — результат замены сохраняется в отдельный Span:
Span<char> destination = stackalloc char[str.Length];
str.AsSpan().Replace(destination, oldChar, newChar);
I continue to compare the string and ReadOnlySpan<char> methods.
The Split method works 46% – 64% faster with ReadOnlySpan<char>. Using the MemoryExtensions.Split method allows to work with Range. An array of ranges can be created on the stack:
Span<Range> ranges = stackalloc Range[str.Length];
str.AsSpan().Split(ranges, separator);
The ToLower and ToUpper methods are also 43% – 45% faster with ReadOnlySpan<char>. Again, this comes from avoiding allocations on a heap:
Span<char> destination = stackalloc char[str.Length];
str.AsSpan().ToLower(destination, CultureInfo.InvariantCulture);
Method CopyTo has almost the same performance — ReadOnlySpan is only 3% faster. However, it's important to note that the destination array should be pre-initialized. This can be done either on the stack using stackalloc or on the heap using the new operator, which can affect performance:
// Option 1
Span<char> destination = new char[str.Length];
str.CopyTo(destination);
// Option 2
Span<char> destination = stackalloc char[str.Length];
str.CopyTo(destination);
Method Replace in ReadOnlySpan<char> is 75% faster than in string. Again, this is due to the absence of heap allocations when using ReadOnlySpan:
Span<char> destination = stackalloc char[str.Length];
str.AsSpan().Replace(destination, oldChar, newChar);
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10
English is below.
Опытные литкодеры знают, насколько нестабильны результаты бенчмарков решений на LeetCode. Одно и то же решение может показывать результаты с большим разбросом. Видимо, они начали что-то с этим делать и на днях изменили подход к замерам:
«Previously, the runtime calculation included overhead processes, but now we only measure the actual time your code takes to run.»
Вольный перевод: Раньше в замеры попадало время холодного старта приложения, а теперь измеряется только время выполнения кода.
С точки зрения бенчмаркинга, это правильное решение. Время холодного старта может значительно искажать результаты микробенчмарков. Однако, это изменение сломало систему рейтинга существующих решений.
Для тех, кто ориентируется только на Big O нотацию, это не должно создать проблем. Остальным придётся смириться с тем, что их старые результаты станут невалидны. Новые решения могут выполняться всего за несколько миллисекунд, тогда как этот же код ранее выполнялся за 100+ мс. При этом на диаграмме отображаются как новые результаты, так и старые, что приводит к тому, что почти любая новая отправка решения приводит к «Beats 100.00%».
Experienced LeetCoders know how unstable the benchmark results can be on LeetCode. The same solution can show results with a big variation. It seems they started fixing it and recently changed the way they measure performance:
«Previously, the runtime calculation included overhead processes, but now we only measure the actual time your code takes to run.»
From a benchmarking perspective, this is the right decision. Cold start can significantly spoil the results of microbenchmarks. However, this change has broken the rating system for existing solutions.
For those who focus only on Big O notation, this shouldn't create any problems. All others have to accept that their old results became invalid. New solutions may run in just a 1-2 milliseconds, while the same code previously ran in over 100 ms. At the same time, both new and old results are displayed on the diagram, leading to nearly any new submission of a solution being marked as «Beats 100.00%».
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
English is below.
На просторах опенсорс-сообщества разгорается новый скандал. Вчера были удалены упоминания мейнтейнеров ядра Linux, чья почта зарегистрирована в зоне *.ru. А сегодня сам Линус Торвальдс накинул 💩 на вентилятор:
Ok, lots of Russian trolls out and about.
It's entirely clear why the change was done, it's not getting reverted, and using multiple random anonymous accounts to try to "grass root" it by Russian troll factories isn't going to change anything.
And FYI for the actual innocent bystanders who aren't troll farm accounts - the "various compliance requirements" are not just a US thing.
If you haven't heard of Russian sanctions yet, you should try to read the news some day. And by "news", I don't mean Russian
state-sponsored spam.
As to sending me a revert patch - please use whatever mush you call brains. I'm Finnish. Did you think I'd be *supporting* Russian
aggression? Apparently it's not just lack of real news, it's lack of history knowledge too.
Не знаю, к чему всё это приведёт, но печально видеть, как железные занавесы опускаются с обеих сторон, даже в таком интернациональном сообществе, как опенсорс.
A new scandal is growing in the open-source community. Yesterday, mentions of Linux kernel maintainers with emails in the *.ru domain were removed. And today, Linus Torvalds itself made the situation worse by writing this.
I don’t know where all this will lead, but it’s sad to see the iron curtains closing from both sides, even in a community as international as open-source.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3👎3
Сейчас я работаю над статьёй о том, как писать производительный код для работы с коллекциями. В ней будут как базовые советы для начинающих программистов, так и продвинутые. Поскольку статья ещё не готова, держите мем и фрагмент, посвящённый LINQ.
---
В большинстве случаев методы LINQ работают медленнее и используют больше памяти. Это не значит, что LINQ – это плохо. Нет, это очень крутая фича C# и, естественно, я её тоже использую. Однако в некоторых и, подчеркну, редких ситуациях для лучшей производительности стоит писать в императивном стиле. Рассмотрим несколько простых примеров.
Select vs ConvertAll vs императивный стиль
Предположим, у нас есть массив транзакций:
public record class Transaction(
Guid Id,
int Amount,
string Denoscription);
Допустим, нужно получить поле Denoscription для всех элементов массива _transactions. Неважно, зачем нам это, — просто нужно. 🙂 Это можно сделать с помощью Array.ConvertAll, методов Select и ToArray, или вручную. Сравним эти три способа.
// Benchmark 1
Array.ConvertAll(_transactions,
x => x.Denoscription);
// Benchmark 2
_transactions
.Select(x => x.Denoscription)
.ToArray();
// Benchamrk 3
var array = new string[_transactions.Length];
for (int i = 0; i < _transactions.Length; i++) {
array[i] = _transactions[i].Denoscription;
}
// Results
| Method | Mean | Ratio |
|------------------ |-----------:|---------:|
| ArrayConvertAll | 1,296.4 μs | -3% |
| LinqSelect | 1,343.4 μs | baseline |
| ImperativeConvert | 1,250.4 μs | -7% |
Array.ConvertAll работает на 3% быстрее, чем LINQ, а императивный стиль — на 7% быстрее. Да, разница небольшая, но методы LINQ часто используются последовательно, и в итоге суммарное отличие в производительности может оказаться более заметным.
Any vs Exists vs императивный стиль
Рассмотрим второй пример — проверка существования элемента в массиве.
// Benchmark 1
return Array.Exists(_transactions,
x => x.Amount > 1_000_00);
// Benchmark 2
return _transactions
.Any(x => x.Amount > 1_000_00);
// Benchmark 3
foreach (var t in _transactions) {
if (t.Amount > 1_000_000) return true;
}
return false;
// Results
| Method | Mean | Ratio |
|------------------ |-----------:|---------:|
| ArrayExists | 567.9 μs | -39% |
| LinqAny | 926.1 μs | baseline |
| ImperativeExists | 488.5 μs | -47% |
Array.Exists работает на 39% быстрее, чем LINQ, а императивный стиль — почти вдвое быстрее.
---
Я обязательно проверю и другие методы, а также протестирую с массивами разного размера. Но что-то мне подсказывает, что этот раздел будет называться «Избегайте LINQ».
Кстати, напишите в комментариях, с какими проблемами производительности вам приходилось сталкиваться при работе с коллекциями. Если этого ещё нет в статье, я проанализирую, сделаю бенчмарки и включу разбор в статью.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
Публикую следующую часть статьи про производительность коллекций. Сегодня про Enumerator.
---
Предположим, у нас есть массив _transactionsArray и список _transactionsList. Сама транзакция выглядит следующим образом:
public record class Transaction(
Guid Id,
int Amount,
string Denoscription);
Существует множество способов пройтись по двум вышеупомянутым коллекциям:
// Индексатор массива
var sum = 0;
for (var i = 0; i < _transactionsArray.List; i++) {
sum += _transactionsArray[i].Amount;
}
// Индексатор списка
var sum = 0;
for (var i = 0; i < _transactionsList.Count; i++) {
sum += _transactionsList[i].Amount;
}
// Энумератор IEnumerable<T>
var sum = 0;
var collection = (IEnumerable<T>)_transactionsArray;
foreach (var item in collection) {
sum += item.Amount;
}
var sum = 0;
var collection = (IEnumerable<T>)_transactionsList;
foreach (var item in collection) {
sum += item.Amount;
}
// Энумератор List<T>
var sum = 0;
foreach (var item in _transactionsList) {
sum += item.Amount;
}
Один из этих способов аллоцирует больше памяти, чем остальные – это приведение _transactionsList к IEnumerable<T>. Причина кроется в реализации энумератора для списков. У типа List<T> есть собственный энумератор-структура.
Если использовать foreach с типом List<T>, то проблем не возникает. Компилятор C# сгенерирует код, который будет использовать эту структуру напрямую.
// Исходный код
var nums= new List<int>();
var sum = 0;
foreach (n in nums) sum +=n;
// Код после компиляции
var nums = new List<int>();
var num = 0;
List<int>.Enumerator enumerator =
nums.GetEnumerator();
while (enumerator.MoveNext()) {
num += enumerator.Current;
}
Но если привести List<T>, например, к IList<T> или IReadOnlyList<T>, т.е. к любому интерфейсу, реализующему IEnumerable<T>, то произойдёт неявная упаковка List<T>.Enumerator. Это происходит из-за того, что IEnumerable<T>.GetEnumerator() возвращает интерфейс IEnumerator<T>.
// Исходный код
var nums = new List<int>();
var collection = (IEnumerable<int>) nums;
var sum = 0;
foreach (n in collection) sum +=n;
// Код после компиляции
var nums = new List<int>();
var collection = ((IEnumerable<int>)nums);
var num = 0;
IEnumerator<int>.Enumerator enumerator =
// упаковка
collection.GetEnumerator();
while (enumerator.MoveNext()) {
num += enumerator.Current;
}
Аналогичный подход с энумераторами также встречается и других коллекциях: LinkedList<T>, Stack<T>, Queue<T> и т.д. Исключением являются, например, массивы.
Насколько упаковка влияет на производительность можно понять из графиков. Бенчмарк 100 раз прошёлся по каждой из коллекции. На первом графике ось Y отображает проценты от бенчмарка с массивом, а на втором — миллисекунды. Шкала оси X в обоих случаях логарифмическая.
Если говорить об абсолютных значениях, разница не так велика — десятки миллисекунд для коллекций размером 100 000 элементов и более. Наибольшее коварство такое поведение представляет, когда у вас много небольших коллекций. Я в своей практике встречал проблему, когда упаковка энумератора приводила к аллокациям десятков и сотен мегабайт. Это было большое дерево директорий, а доступ к дочерним директориям был только через IReadOnlyList<T>.
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
Используйте диапазоны (Range) только со Span<int>
В предыдущем посте я писал о скрытой аллокации, возникающей из-за упаковки энумератора. В этом посте расскажу об ещё одной скрытой аллокации.
5 лет назад в C# 8.0 появились индексы и диапазоны, которые позволяют получать часть коллекции с помощью удобного синтаксиса. Например, в следующем примере из массива извлекаются все элементы, кроме 2-х первых и 2-х последних:
Это удобно, особенно по сравнению с аналогичной записью через LINQ:
В случае с LINQ мы явно создаём массив с помощью метода ToArray. Однако это можно и не делать, если нужно просто пройтись по выбранным элементам.
Теперь посмотрим на C#-код без синтаксического сахара. При использовании с массивом оператор диапазона всегда создаёт новый массив. При компиляции он преобразуется в вызов RuntimeHelpers.GetSubArray, который и создаёт новый массив.
Если нет необходимости сохранять подмассив, лучше вызвать AsSpan() перед использованием диапазонов.
С точки зрения производительности, создание нового массива и копирование элементов – это затратная операция, особенно в сравнении со слайсами спанов, которые выполняются практически мгновенно. Рассмотрим следующий код:
Добавление AsSpan() перед использованием оператора диапазона позволяет сократить время выполнения в среднем в 2-3 раза (см. график).
В предыдущем посте я писал о скрытой аллокации, возникающей из-за упаковки энумератора. В этом посте расскажу об ещё одной скрытой аллокации.
5 лет назад в C# 8.0 появились индексы и диапазоны, которые позволяют получать часть коллекции с помощью удобного синтаксиса. Например, в следующем примере из массива извлекаются все элементы, кроме 2-х первых и 2-х последних:
int[] arr = [1, 2, 3, 4, 5, 6];
var subarr = arr[2..^2]; // [3, 4]
Это удобно, особенно по сравнению с аналогичной записью через LINQ:
var subarr = arr.Skip(2)
.Take(arr.Length - 2 - 2)
.ToArray();
В случае с LINQ мы явно создаём массив с помощью метода ToArray. Однако это можно и не делать, если нужно просто пройтись по выбранным элементам.
Теперь посмотрим на C#-код без синтаксического сахара. При использовании с массивом оператор диапазона всегда создаёт новый массив. При компиляции он преобразуется в вызов RuntimeHelpers.GetSubArray, который и создаёт новый массив.
// int[] subarr = arr[2..^2]
int[] subArray = RuntimeHelpers
.GetSubArray(array, new Range(2,
new Index(2, true)));
Если нет необходимости сохранять подмассив, лучше вызвать AsSpan() перед использованием диапазонов.
// Исходный код
int[] arr = [1, 2, 3, 4, 5, 6];
var subarr = arr.AsSpan()[2..^2]; // [3, 4]
// После компиляции
Span<int> span = MemoryExtensions.AsSpan(array);
Span<int> subarr = span.Slice(2,
span.Length - 2 - 2);
С точки зрения производительности, создание нового массива и копирование элементов – это затратная операция, особенно в сравнении со слайсами спанов, которые выполняются практически мгновенно. Рассмотрим следующий код:
start = Length / 4;
end = Length * 3 / 4;
var sum = 0;
// 2-3 times faster:
// _transactionsArray.AsSpan()[_start.._end];
var sliced = _transactionsArray[start..end];
foreach (var t in sliced) sum += t.Amount;
return sum;
Добавление AsSpan() перед использованием оператора диапазона позволяет сократить время выполнения в среднем в 2-3 раза (см. график).
👍7
EventFlow – опенсорс библиотека для DDD, Event Sourcing и CQRS
Немного отвлечёмся от бенчмарков. Уже 2 недели работаю над pet-проектом с DDD + ES + CQRS. Для этого искал подходящую библиотеку, т.к. хотелось сосредоточиться на бизнес-проблемах, а не изобретать велосипед. Нашёл фреймворк EventFlow. В библиотеке есть всё готовое:
- базовые классы для агрегатов и доменных событий;
- команды, запросы;
- обработчики команд, запросов, событий;
- функционал для паттерна «сага»;
- функционал для миграций событий.
и многое другое.
Документация написана достаточно подробно, главное внимательно читать. Я, например, проглядел, что по умолчанию фреймворк «проглатывает» исключения, выброшенные в хэндлерах команд. Из-за этого долго не мог понять, почему некоторые тестовые моки интерфейсов выбрасывают исключения, но это не приводит к поломке тестов.
Альтернативы
В процессе поиска нашёл несколько других проектов по теме. Возможно, кому-то пригодится, поэтому делюсь списком.
1. Revo – ещё одна библиотека для DDD, Event Sourcing и CQRS. Изначально я попытался запустить проект именно с ней, но мне она не понравилась по нескольким причинам:
- Проект крэшился сразу после запуска. Связано это с тем, что Revo построен вокруг Ninject – сторонней библиотеки для внедрения зависимостей, а не встроенного в .NET DI. Исправить я это не смог, из-за плохо написанной документации.
- Функционал библиотеки разбит на множество Nuget-пакетов. Пришлось несколько раз возвращаться к документации, чтобы понять какой очередной пакет установить, чтобы проект наконец-то уже собрался.
У проекта много звёзд, так что, возможно, библиотека не так плоха, а это я не смог разобраться. 🤷♂️
2. Marten DB – Event Store построенный вокруг PostgreSQL. Подходит, если вы хотите самостоятельно разработать базовые классы для агрегатов, событий, команд, запросов, шин сообщений и т. д.
3. YesSQL – интерфейс, имитирующий документоориентированную БД. Библиотека построена поверх EF Core и работает c SQLite, PostgreSQL, SQL Server и MySQL. Думаю, что тоже подходит для создания Event Store.
Немного отвлечёмся от бенчмарков. Уже 2 недели работаю над pet-проектом с DDD + ES + CQRS. Для этого искал подходящую библиотеку, т.к. хотелось сосредоточиться на бизнес-проблемах, а не изобретать велосипед. Нашёл фреймворк EventFlow. В библиотеке есть всё готовое:
- базовые классы для агрегатов и доменных событий;
- команды, запросы;
- обработчики команд, запросов, событий;
- функционал для паттерна «сага»;
- функционал для миграций событий.
и многое другое.
Документация написана достаточно подробно, главное внимательно читать. Я, например, проглядел, что по умолчанию фреймворк «проглатывает» исключения, выброшенные в хэндлерах команд. Из-за этого долго не мог понять, почему некоторые тестовые моки интерфейсов выбрасывают исключения, но это не приводит к поломке тестов.
Альтернативы
В процессе поиска нашёл несколько других проектов по теме. Возможно, кому-то пригодится, поэтому делюсь списком.
1. Revo – ещё одна библиотека для DDD, Event Sourcing и CQRS. Изначально я попытался запустить проект именно с ней, но мне она не понравилась по нескольким причинам:
- Проект крэшился сразу после запуска. Связано это с тем, что Revo построен вокруг Ninject – сторонней библиотеки для внедрения зависимостей, а не встроенного в .NET DI. Исправить я это не смог, из-за плохо написанной документации.
- Функционал библиотеки разбит на множество Nuget-пакетов. Пришлось несколько раз возвращаться к документации, чтобы понять какой очередной пакет установить, чтобы проект наконец-то уже собрался.
У проекта много звёзд, так что, возможно, библиотека не так плоха, а это я не смог разобраться. 🤷♂️
2. Marten DB – Event Store построенный вокруг PostgreSQL. Подходит, если вы хотите самостоятельно разработать базовые классы для агрегатов, событий, команд, запросов, шин сообщений и т. д.
3. YesSQL – интерфейс, имитирующий документоориентированную БД. Библиотека построена поверх EF Core и работает c SQLite, PostgreSQL, SQL Server и MySQL. Думаю, что тоже подходит для создания Event Store.
👍7
Около месяца назад я вписался в C# Advent Calendar — онлайн-мероприятие, организованное Мэтью Гроувсом, энтузиастом C# и автором книги «Аспектно-ориентированное программирование на .NET».
Суть мероприятия в следующем: с 1 по 25 декабря на сайте csadvent.christmas дважды в день публикуются статьи от авторов о разработке на C# и .NET. Моя статья выйдет 9 декабря. Я как раз завершаю над ней работу. В статье я расскажу об одном интересном паттерне для высокой производительности: будут примеры реализации паттерна на C#, разбор внутренностей, бенчмарки и графики. Статья будет на английском, но сюда обязательно опубликую перевод.
Если в адвенте будут интересные статьи, то раз в неделю буду делиться подборкой.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍21
Как и обещал, выкладываю самое интересное за первую неделю на C# Advent 2024.
Очень интересный челлендж со строками. Нужно сделать так, что бы программа ниже вывела “Advent of C#”.
Initialize();
var happyHolidays = "Merry Christmas";
// should output: Advent of C#
Console.WriteLine(happyHolidays);
static void Initialize() {
// Your implementation here
}
Сложность в том, что:
- Изменения можно вносить только в метод Initialize.
- Нельзя менять сигнатуру метода.
- Нельзя использовать Console внутри метода Initialize.
- Нельзя использовать стандартный выходной поток.
Автор рассматривает несколько решений. Все они связаны с поведением .NET при работе со строками-литералами, пулом интернирования строк и указателями.
Статья о том, как избежать проблем с множеством зависимостей в классах. Но я бы назвал её «Как не надо проектировать системы». Честно говоря, я не представляю, как можно получить контроллер, в который внедряются 34 зависимости. 😵💫 Это не шутка – смотрите пример из статьи.
Благодаря Minimal API и Primary Constructors автор решает проблему с большим количеством зависимостей, но мне кажется, корень проблемы лежит немного в другой плоскости.
---
В понедельник утром публикуется моя статья. Версию на русском опубликую на Хабре.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10